diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 2df37fec14..e11a34a798 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -672,6 +672,7 @@ func registerControllers(mgr manager.Manager, params operator.Parameters, access {name: "APM-ES", registerFunc: associationctl.AddApmES}, {name: "APM-KB", registerFunc: associationctl.AddApmKibana}, {name: "KB-ES", registerFunc: associationctl.AddKibanaES}, + {name: "KB-ENT", registerFunc: associationctl.AddKibanaEnt}, {name: "ENT-ES", registerFunc: associationctl.AddEntES}, {name: "BEAT-ES", registerFunc: associationctl.AddBeatES}, {name: "BEAT-KB", registerFunc: associationctl.AddBeatKibana}, @@ -708,7 +709,7 @@ func garbageCollectUsers(cfg *rest.Config, managedNamespaces []string) { } err = ugc. For(&apmv1.ApmServerList{}, associationctl.ApmAssociationLabelNamespace, associationctl.ApmAssociationLabelName). - For(&kbv1.KibanaList{}, associationctl.KibanaESAssociationLabelNamespace, associationctl.KibanaESAssociationLabelName). + For(&kbv1.KibanaList{}, associationctl.KibanaAssociationLabelNamespace, associationctl.KibanaAssociationLabelName). For(&entv1.EnterpriseSearchList{}, associationctl.EntESAssociationLabelNamespace, associationctl.EntESAssociationLabelName). For(&beatv1beta1.BeatList{}, associationctl.BeatAssociationLabelNamespace, associationctl.BeatAssociationLabelName). For(&agentv1alpha1.AgentList{}, associationctl.AgentAssociationLabelNamespace, associationctl.AgentAssociationLabelName). diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index 57e1892187..8dbd3dd8d6 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -6136,6 +6136,28 @@ spec: required: - name type: object + enterpriseSearchRef: + description: EnterpriseSearchRef is a reference to an EnterpriseSearch + running in the same Kubernetes cluster. Kibana provides the default + Enterprise Search UI starting version 7.14. + properties: + name: + description: Name of the Kubernetes object. + type: string + namespace: + description: Namespace of the Kubernetes object. If empty, defaults + to the current namespace. + type: string + serviceName: + description: ServiceName is the name of an existing Kubernetes + service which will be used to make requests to the referenced + object. It has to be in the same namespace as the referenced + resource. If left empty the default HTTP service of the referenced + resource will be used. + type: string + required: + - name + type: object http: description: HTTP holds the HTTP layer configuration for Kibana. properties: @@ -6630,13 +6652,23 @@ spec: description: KibanaStatus defines the observed state of Kibana properties: associationStatus: - description: AssociationStatus is the status of an association resource. + description: AssociationStatus is the status of any auto-linking to + Elasticsearch clusters. This field is deprecated and will be removed + in a future release. Use ElasticsearchAssociationStatus instead. type: string availableNodes: description: AvailableNodes is the number of available replicas in the deployment. format: int32 type: integer + elasticsearchAssociationStatus: + description: ElasticsearchAssociationStatus is the status of any auto-linking + to Elasticsearch clusters. + type: string + enterpriseSearchAssociationStatus: + description: EnterpriseSearchAssociationStatus is the status of any + auto-linking to Enterprise Search. + type: string health: description: Health of the deployment. type: string diff --git a/config/crds/v1/bases/kibana.k8s.elastic.co_kibanas.yaml b/config/crds/v1/bases/kibana.k8s.elastic.co_kibanas.yaml index a9130bc990..78027af616 100644 --- a/config/crds/v1/bases/kibana.k8s.elastic.co_kibanas.yaml +++ b/config/crds/v1/bases/kibana.k8s.elastic.co_kibanas.yaml @@ -84,6 +84,28 @@ spec: required: - name type: object + enterpriseSearchRef: + description: EnterpriseSearchRef is a reference to an EnterpriseSearch + running in the same Kubernetes cluster. Kibana provides the default + Enterprise Search UI starting version 7.14. + properties: + name: + description: Name of the Kubernetes object. + type: string + namespace: + description: Namespace of the Kubernetes object. If empty, defaults + to the current namespace. + type: string + serviceName: + description: ServiceName is the name of an existing Kubernetes + service which will be used to make requests to the referenced + object. It has to be in the same namespace as the referenced + resource. If left empty the default HTTP service of the referenced + resource will be used. + type: string + required: + - name + type: object http: description: HTTP holds the HTTP layer configuration for Kibana. properties: @@ -7263,13 +7285,23 @@ spec: description: KibanaStatus defines the observed state of Kibana properties: associationStatus: - description: AssociationStatus is the status of an association resource. + description: AssociationStatus is the status of any auto-linking to + Elasticsearch clusters. This field is deprecated and will be removed + in a future release. Use ElasticsearchAssociationStatus instead. type: string availableNodes: description: AvailableNodes is the number of available replicas in the deployment. format: int32 type: integer + elasticsearchAssociationStatus: + description: ElasticsearchAssociationStatus is the status of any auto-linking + to Elasticsearch clusters. + type: string + enterpriseSearchAssociationStatus: + description: EnterpriseSearchAssociationStatus is the status of any + auto-linking to Enterprise Search. + type: string health: description: Health of the deployment. type: string diff --git a/config/crds/v1beta1/all-crds.yaml b/config/crds/v1beta1/all-crds.yaml index b7fee80869..ae8b32da8b 100644 --- a/config/crds/v1beta1/all-crds.yaml +++ b/config/crds/v1beta1/all-crds.yaml @@ -3801,6 +3801,28 @@ spec: required: - name type: object + enterpriseSearchRef: + description: EnterpriseSearchRef is a reference to an EnterpriseSearch + running in the same Kubernetes cluster. Kibana provides the default + Enterprise Search UI starting version 7.14. + properties: + name: + description: Name of the Kubernetes object. + type: string + namespace: + description: Namespace of the Kubernetes object. If empty, defaults + to the current namespace. + type: string + serviceName: + description: ServiceName is the name of an existing Kubernetes service + which will be used to make requests to the referenced object. + It has to be in the same namespace as the referenced resource. + If left empty the default HTTP service of the referenced resource + will be used. + type: string + required: + - name + type: object http: description: HTTP holds the HTTP layer configuration for Kibana. properties: @@ -4258,13 +4280,23 @@ spec: description: KibanaStatus defines the observed state of Kibana properties: associationStatus: - description: AssociationStatus is the status of an association resource. + description: AssociationStatus is the status of any auto-linking to + Elasticsearch clusters. This field is deprecated and will be removed + in a future release. Use ElasticsearchAssociationStatus instead. type: string availableNodes: description: AvailableNodes is the number of available replicas in the deployment. format: int32 type: integer + elasticsearchAssociationStatus: + description: ElasticsearchAssociationStatus is the status of any auto-linking + to Elasticsearch clusters. + type: string + enterpriseSearchAssociationStatus: + description: EnterpriseSearchAssociationStatus is the status of any + auto-linking to Enterprise Search. + type: string health: description: Health of the deployment. type: string diff --git a/config/crds/v1beta1/bases/kibana.k8s.elastic.co_kibanas.yaml b/config/crds/v1beta1/bases/kibana.k8s.elastic.co_kibanas.yaml index 514b5d80cf..2c2a6d4d4d 100644 --- a/config/crds/v1beta1/bases/kibana.k8s.elastic.co_kibanas.yaml +++ b/config/crds/v1beta1/bases/kibana.k8s.elastic.co_kibanas.yaml @@ -87,6 +87,28 @@ spec: required: - name type: object + enterpriseSearchRef: + description: EnterpriseSearchRef is a reference to an EnterpriseSearch + running in the same Kubernetes cluster. Kibana provides the default + Enterprise Search UI starting version 7.14. + properties: + name: + description: Name of the Kubernetes object. + type: string + namespace: + description: Namespace of the Kubernetes object. If empty, defaults + to the current namespace. + type: string + serviceName: + description: ServiceName is the name of an existing Kubernetes + service which will be used to make requests to the referenced + object. It has to be in the same namespace as the referenced + resource. If left empty the default HTTP service of the referenced + resource will be used. + type: string + required: + - name + type: object http: description: HTTP holds the HTTP layer configuration for Kibana. properties: @@ -7211,13 +7233,23 @@ spec: description: KibanaStatus defines the observed state of Kibana properties: associationStatus: - description: AssociationStatus is the status of an association resource. + description: AssociationStatus is the status of any auto-linking to + Elasticsearch clusters. This field is deprecated and will be removed + in a future release. Use ElasticsearchAssociationStatus instead. type: string availableNodes: description: AvailableNodes is the number of available replicas in the deployment. format: int32 type: integer + elasticsearchAssociationStatus: + description: ElasticsearchAssociationStatus is the status of any auto-linking + to Elasticsearch clusters. + type: string + enterpriseSearchAssociationStatus: + description: EnterpriseSearchAssociationStatus is the status of any + auto-linking to Enterprise Search. + type: string health: description: Health of the deployment. type: string diff --git a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds-legacy.yaml b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds-legacy.yaml index f5939ece4b..de77597beb 100644 --- a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds-legacy.yaml +++ b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds-legacy.yaml @@ -3846,6 +3846,28 @@ spec: required: - name type: object + enterpriseSearchRef: + description: EnterpriseSearchRef is a reference to an EnterpriseSearch + running in the same Kubernetes cluster. Kibana provides the default + Enterprise Search UI starting version 7.14. + properties: + name: + description: Name of the Kubernetes object. + type: string + namespace: + description: Namespace of the Kubernetes object. If empty, defaults + to the current namespace. + type: string + serviceName: + description: ServiceName is the name of an existing Kubernetes service + which will be used to make requests to the referenced object. + It has to be in the same namespace as the referenced resource. + If left empty the default HTTP service of the referenced resource + will be used. + type: string + required: + - name + type: object http: description: HTTP holds the HTTP layer configuration for Kibana. properties: @@ -4303,13 +4325,23 @@ spec: description: KibanaStatus defines the observed state of Kibana properties: associationStatus: - description: AssociationStatus is the status of an association resource. + description: AssociationStatus is the status of any auto-linking to + Elasticsearch clusters. This field is deprecated and will be removed + in a future release. Use ElasticsearchAssociationStatus instead. type: string availableNodes: description: AvailableNodes is the number of available replicas in the deployment. format: int32 type: integer + elasticsearchAssociationStatus: + description: ElasticsearchAssociationStatus is the status of any auto-linking + to Elasticsearch clusters. + type: string + enterpriseSearchAssociationStatus: + description: EnterpriseSearchAssociationStatus is the status of any + auto-linking to Enterprise Search. + type: string health: description: Health of the deployment. type: string diff --git a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml index 16297879eb..c1947154f1 100644 --- a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml +++ b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml @@ -6181,6 +6181,28 @@ spec: required: - name type: object + enterpriseSearchRef: + description: EnterpriseSearchRef is a reference to an EnterpriseSearch + running in the same Kubernetes cluster. Kibana provides the default + Enterprise Search UI starting version 7.14. + properties: + name: + description: Name of the Kubernetes object. + type: string + namespace: + description: Namespace of the Kubernetes object. If empty, defaults + to the current namespace. + type: string + serviceName: + description: ServiceName is the name of an existing Kubernetes + service which will be used to make requests to the referenced + object. It has to be in the same namespace as the referenced + resource. If left empty the default HTTP service of the referenced + resource will be used. + type: string + required: + - name + type: object http: description: HTTP holds the HTTP layer configuration for Kibana. properties: @@ -6675,13 +6697,23 @@ spec: description: KibanaStatus defines the observed state of Kibana properties: associationStatus: - description: AssociationStatus is the status of an association resource. + description: AssociationStatus is the status of any auto-linking to + Elasticsearch clusters. This field is deprecated and will be removed + in a future release. Use ElasticsearchAssociationStatus instead. type: string availableNodes: description: AvailableNodes is the number of available replicas in the deployment. format: int32 type: integer + elasticsearchAssociationStatus: + description: ElasticsearchAssociationStatus is the status of any auto-linking + to Elasticsearch clusters. + type: string + enterpriseSearchAssociationStatus: + description: EnterpriseSearchAssociationStatus is the status of any + auto-linking to Enterprise Search. + type: string health: description: Health of the deployment. type: string diff --git a/docs/reference/api-docs.asciidoc b/docs/reference/api-docs.asciidoc index 9d1835de2d..0602aeabc6 100644 --- a/docs/reference/api-docs.asciidoc +++ b/docs/reference/api-docs.asciidoc @@ -1319,6 +1319,7 @@ KibanaSpec holds the specification of a Kibana instance. | *`image`* __string__ | Image is the Kibana Docker image to deploy. | *`count`* __integer__ | Count of Kibana instances to deploy. | *`elasticsearchRef`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-objectselector[$$ObjectSelector$$]__ | ElasticsearchRef is a reference to an Elasticsearch cluster running in the same Kubernetes cluster. +| *`enterpriseSearchRef`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-objectselector[$$ObjectSelector$$]__ | EnterpriseSearchRef is a reference to an EnterpriseSearch running in the same Kubernetes cluster. Kibana provides the default Enterprise Search UI starting version 7.14. | *`config`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-config[$$Config$$]__ | Config holds the Kibana configuration. See: https://www.elastic.co/guide/en/kibana/current/settings.html | *`http`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-httpconfig[$$HTTPConfig$$]__ | HTTP holds the HTTP layer configuration for Kibana. | *`podTemplate`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#podtemplatespec-v1-core[$$PodTemplateSpec$$]__ | PodTemplate provides customisation options (labels, annotations, affinity rules, resource requests, and so on) for the Kibana pods diff --git a/pkg/apis/common/v1/association.go b/pkg/apis/common/v1/association.go index 46a8b39e6b..84b9d6067c 100644 --- a/pkg/apis/common/v1/association.go +++ b/pkg/apis/common/v1/association.go @@ -96,6 +96,9 @@ const ( KibanaConfigAnnotationNameBase = "association.k8s.elastic.co/kb-conf" KibanaAssociationType = "kibana" + EntConfigAnnotationNameBase = "association.k8s.elastic.co/ent-conf" + EntAssociationType = "ent" + AssociationUnknown AssociationStatus = "" AssociationPending AssociationStatus = "Pending" AssociationEstablished AssociationStatus = "Established" @@ -106,6 +109,10 @@ const ( // should use `SingletonAssociationID` as their `AssociationID`. On the contrary, Agent can have unbounded number // of Associations so Agent-ES Associations should _not_ use `SingletonAssociationID`. SingletonAssociationID = "" + + // NoAuthRequiredValue is the value set for AuthSecretName if no authentication + // credentials are necessary for that association. + NoAuthRequiredValue = "-" ) // Associated represents an Elastic stack resource that is associated with other stack resources. @@ -193,14 +200,23 @@ func (ac *AssociationConf) IsConfigured() bool { return ac.AuthIsConfigured() && ac.URLIsConfigured() } -// AuthIsConfigured returns true if all the auth fields are set. +// AuthIsConfigured returns true if the auth fields are set. func (ac *AssociationConf) AuthIsConfigured() bool { if ac == nil { return false } + if ac.NoAuthRequired() { + // auth fields are not required, but still configured + return true + } + // ensure both secret name and secret key are provided return ac.AuthSecretName != "" && ac.AuthSecretKey != "" } +func (ac *AssociationConf) NoAuthRequired() bool { + return ac.AuthSecretName == NoAuthRequiredValue +} + // CAIsConfigured returns true if the CA field is set. func (ac *AssociationConf) CAIsConfigured() bool { if ac == nil { diff --git a/pkg/apis/common/v1/association_test.go b/pkg/apis/common/v1/association_test.go index 08fee60898..0e63f9a0be 100644 --- a/pkg/apis/common/v1/association_test.go +++ b/pkg/apis/common/v1/association_test.go @@ -80,6 +80,15 @@ func TestAssociationConfIsConfigured(t *testing.T) { }, want: true, }, + { + name: "correctly configured with no auth required", + assocConf: &AssociationConf{ + AuthSecretName: "-", + CASecretName: "ca-secret", + URL: "https://my-es.svc", + }, + want: true, + }, } for _, tt := range tests { @@ -252,3 +261,59 @@ func TestAssociationStatusMap_String(t *testing.T) { }) } } + +func TestAssociationConf_AuthIsConfigured(t *testing.T) { + type fields struct { + AuthSecretName string + AuthSecretKey string + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "auth configured", + fields: fields{ + AuthSecretName: "secret-name", + AuthSecretKey: "secret-key", + }, + want: true, + }, + { + name: "auth secret key not configured", + fields: fields{ + AuthSecretName: "secret-name", + AuthSecretKey: "", + }, + want: false, + }, + { + name: "auth not configured", + fields: fields{ + AuthSecretName: "", + AuthSecretKey: "", + }, + want: false, + }, + { + name: "auth not required (but still configured)", + fields: fields{ + AuthSecretName: "-", + AuthSecretKey: "", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ac := &AssociationConf{ + AuthSecretName: tt.fields.AuthSecretName, + AuthSecretKey: tt.fields.AuthSecretKey, + } + if got := ac.AuthIsConfigured(); got != tt.want { + t.Errorf("AuthIsConfigured() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/apis/enterprisesearch/v1/enterprisesearch_types.go b/pkg/apis/enterprisesearch/v1/enterprisesearch_types.go index a616dfae53..510f8158e1 100644 --- a/pkg/apis/enterprisesearch/v1/enterprisesearch_types.go +++ b/pkg/apis/enterprisesearch/v1/enterprisesearch_types.go @@ -8,6 +8,7 @@ import ( "fmt" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" + common_name "github.com/elastic/cloud-on-k8s/pkg/controller/common/name" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -19,6 +20,9 @@ const ( Kind = "EnterpriseSearch" ) +// Namer is a Namer that is configured with the defaults for resources related to an EnterpriseSearch resource. +var Namer = common_name.NewNamer("ent") + // EnterpriseSearchSpec holds the specification of an Enterprise Search resource. type EnterpriseSearchSpec struct { // Version of Enterprise Search. diff --git a/pkg/apis/kibana/v1/kibana_types.go b/pkg/apis/kibana/v1/kibana_types.go index 2883c3dda7..8b053cfceb 100644 --- a/pkg/apis/kibana/v1/kibana_types.go +++ b/pkg/apis/kibana/v1/kibana_types.go @@ -19,6 +19,41 @@ const ( Kind = "Kibana" ) +// +kubebuilder:object:root=true + +// Kibana represents a Kibana resource in a Kubernetes cluster. +// +kubebuilder:resource:categories=elastic,shortName=kb +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="health",type="string",JSONPath=".status.health" +// +kubebuilder:printcolumn:name="nodes",type="integer",JSONPath=".status.availableNodes",description="Available nodes" +// +kubebuilder:printcolumn:name="version",type="string",JSONPath=".status.version",description="Kibana version" +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:storageversion +type Kibana struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KibanaSpec `json:"spec,omitempty"` + Status KibanaStatus `json:"status,omitempty"` + // assocConf holds the configuration for the Elasticsearch association + assocConf *commonv1.AssociationConf `json:"-"` + // entAssocConf holds the configuration for the Enterprise Search association + entAssocConf *commonv1.AssociationConf `json:"-"` +} + +// +kubebuilder:object:root=true + +// KibanaList contains a list of Kibana +type KibanaList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Kibana `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Kibana{}, &KibanaList{}) +} + // KibanaSpec holds the specification of a Kibana instance. type KibanaSpec struct { // Version of Kibana. @@ -33,6 +68,10 @@ type KibanaSpec struct { // ElasticsearchRef is a reference to an Elasticsearch cluster running in the same Kubernetes cluster. ElasticsearchRef commonv1.ObjectSelector `json:"elasticsearchRef,omitempty"` + // EnterpriseSearchRef is a reference to an EnterpriseSearch running in the same Kubernetes cluster. + // Kibana provides the default Enterprise Search UI starting version 7.14. + EnterpriseSearchRef commonv1.ObjectSelector `json:"enterpriseSearchRef,omitempty"` + // Config holds the Kibana configuration. See: https://www.elastic.co/guide/en/kibana/current/settings.html // +kubebuilder:pruning:PreserveUnknownFields Config *commonv1.Config `json:"config,omitempty"` @@ -57,7 +96,13 @@ type KibanaSpec struct { // KibanaStatus defines the observed state of Kibana type KibanaStatus struct { commonv1.DeploymentStatus `json:",inline"` - AssociationStatus commonv1.AssociationStatus `json:"associationStatus,omitempty"` + // AssociationStatus is the status of any auto-linking to Elasticsearch clusters. + // This field is deprecated and will be removed in a future release. Use ElasticsearchAssociationStatus instead. + AssociationStatus commonv1.AssociationStatus `json:"associationStatus,omitempty"` + // ElasticsearchAssociationStatus is the status of any auto-linking to Elasticsearch clusters. + ElasticsearchAssociationStatus commonv1.AssociationStatus `json:"elasticsearchAssociationStatus,omitempty"` + // EnterpriseSearchAssociationStatus is the status of any auto-linking to Enterprise Search. + EnterpriseSearchAssociationStatus commonv1.AssociationStatus `json:"enterpriseSearchAssociationStatus,omitempty"` } // IsMarkedForDeletion returns true if the Kibana is going to be deleted @@ -65,22 +110,6 @@ func (k *Kibana) IsMarkedForDeletion() bool { return !k.DeletionTimestamp.IsZero() } -func (k *Kibana) Associated() commonv1.Associated { - return k -} - -func (k *Kibana) AssociationConfAnnotationName() string { - return commonv1.ElasticsearchConfigAnnotationNameBase -} - -func (k *Kibana) AssociationType() commonv1.AssociationType { - return commonv1.ElasticsearchAssociationType -} - -func (k *Kibana) AssociationRef() commonv1.ObjectSelector { - return k.Spec.ElasticsearchRef.WithDefaultNamespace(k.Namespace) -} - func (k *Kibana) SecureSettings() []commonv1.SecretSource { return k.Spec.SecureSettings } @@ -89,22 +118,41 @@ func (k *Kibana) ServiceAccountName() string { return k.Spec.ServiceAccountName } -func (k *Kibana) AssociationConf() *commonv1.AssociationConf { - return k.assocConf -} +// -- associations + +var _ commonv1.Associated = &Kibana{} -func (k *Kibana) SetAssociationConf(assocConf *commonv1.AssociationConf) { - k.assocConf = assocConf +func (k *Kibana) Associated() commonv1.Associated { + return k } -// RequiresAssociation returns true if the spec specifies an Elasticsearch reference. -func (k *Kibana) RequiresAssociation() bool { - return k.Spec.ElasticsearchRef.Name != "" +func (k *Kibana) GetAssociations() []commonv1.Association { + associations := make([]commonv1.Association, 0) + + if k.Spec.ElasticsearchRef.IsDefined() { + associations = append(associations, &KibanaEsAssociation{ + Kibana: k, + }) + } + if k.Spec.EnterpriseSearchRef.IsDefined() { + associations = append(associations, &KibanaEntAssociation{ + Kibana: k, + }) + } + + return associations } func (k *Kibana) AssociationStatusMap(typ commonv1.AssociationType) commonv1.AssociationStatusMap { - if typ == commonv1.ElasticsearchAssociationType && k.Spec.ElasticsearchRef.IsDefined() { - return commonv1.NewSingleAssociationStatusMap(k.Status.AssociationStatus) + switch typ { + case commonv1.ElasticsearchAssociationType: + if k.Spec.ElasticsearchRef.IsDefined() { + return commonv1.NewSingleAssociationStatusMap(k.Status.ElasticsearchAssociationStatus) + } + case commonv1.EntAssociationType: + if k.Spec.EnterpriseSearchRef.IsDefined() { + return commonv1.NewSingleAssociationStatusMap(k.Status.EnterpriseSearchAssociationStatus) + } } return commonv1.AssociationStatusMap{} @@ -116,57 +164,111 @@ func (k *Kibana) SetAssociationStatusMap(typ commonv1.AssociationType, status co return err } - if typ != commonv1.ElasticsearchAssociationType { + switch typ { + case commonv1.ElasticsearchAssociationType: + k.Status.ElasticsearchAssociationStatus = single + // also set Status.AssociationStatus to report the status of the association with es, + // for backward compatibility reasons + k.Status.AssociationStatus = single + return nil + case commonv1.EntAssociationType: + k.Status.EnterpriseSearchAssociationStatus = single + return nil + default: return fmt.Errorf("association type %s not known", typ) } +} - k.Status.AssociationStatus = single - return nil +// -- association with Elasticsearch + +func (k *Kibana) EsAssociation() *KibanaEsAssociation { + return &KibanaEsAssociation{Kibana: k} } -func (k *Kibana) GetAssociations() []commonv1.Association { - associations := make([]commonv1.Association, 0) - if k.Spec.ElasticsearchRef.IsDefined() { - associations = append(associations, k) +// KibanaEsAssociation helps to manage the Kibana / Elasticsearch association. +type KibanaEsAssociation struct { + *Kibana +} + +var _ commonv1.Association = &KibanaEsAssociation{} + +func (kbes *KibanaEsAssociation) Associated() commonv1.Associated { + if kbes == nil { + return nil } - return associations + if kbes.Kibana == nil { + kbes.Kibana = &Kibana{} + } + return kbes.Kibana +} + +func (kbes *KibanaEsAssociation) AssociationConfAnnotationName() string { + return commonv1.ElasticsearchConfigAnnotationNameBase +} + +func (kbes *KibanaEsAssociation) AssociationType() commonv1.AssociationType { + return commonv1.ElasticsearchAssociationType +} + +func (kbes *KibanaEsAssociation) AssociationRef() commonv1.ObjectSelector { + return kbes.Spec.ElasticsearchRef.WithDefaultNamespace(kbes.Namespace) +} + +func (kbes *KibanaEsAssociation) AssociationConf() *commonv1.AssociationConf { + return kbes.assocConf +} + +func (kbes *KibanaEsAssociation) SetAssociationConf(assocConf *commonv1.AssociationConf) { + kbes.assocConf = assocConf } -func (k *Kibana) AssociationID() string { +func (kbes *KibanaEsAssociation) AssociationID() string { return commonv1.SingletonAssociationID } -var _ commonv1.Associated = &Kibana{} -var _ commonv1.Association = &Kibana{} +// -- association with Enterprise Search -// +kubebuilder:object:root=true +func (k *Kibana) EntAssociation() *KibanaEntAssociation { + return &KibanaEntAssociation{Kibana: k} +} -// Kibana represents a Kibana resource in a Kubernetes cluster. -// +kubebuilder:resource:categories=elastic,shortName=kb -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="health",type="string",JSONPath=".status.health" -// +kubebuilder:printcolumn:name="nodes",type="integer",JSONPath=".status.availableNodes",description="Available nodes" -// +kubebuilder:printcolumn:name="version",type="string",JSONPath=".status.version",description="Kibana version" -// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" -// +kubebuilder:storageversion -type Kibana struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` +// KibanaEntAssociation helps to manage the Kibana / Enterprise Search association. +type KibanaEntAssociation struct { + *Kibana +} - Spec KibanaSpec `json:"spec,omitempty"` - Status KibanaStatus `json:"status,omitempty"` - assocConf *commonv1.AssociationConf `json:"-"` +var _ commonv1.Association = &KibanaEntAssociation{} + +func (kbent *KibanaEntAssociation) Associated() commonv1.Associated { + if kbent == nil { + return nil + } + if kbent.Kibana == nil { + kbent.Kibana = &Kibana{} + } + return kbent.Kibana } -// +kubebuilder:object:root=true +func (kbent *KibanaEntAssociation) AssociationConfAnnotationName() string { + return commonv1.EntConfigAnnotationNameBase +} -// KibanaList contains a list of Kibana -type KibanaList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Kibana `json:"items"` +func (kbent *KibanaEntAssociation) AssociationType() commonv1.AssociationType { + return commonv1.EntAssociationType } -func init() { - SchemeBuilder.Register(&Kibana{}, &KibanaList{}) +func (kbent *KibanaEntAssociation) AssociationRef() commonv1.ObjectSelector { + return kbent.Spec.EnterpriseSearchRef.WithDefaultNamespace(kbent.Namespace) +} + +func (kbent *KibanaEntAssociation) AssociationConf() *commonv1.AssociationConf { + return kbent.entAssocConf +} + +func (kbent *KibanaEntAssociation) SetAssociationConf(assocConf *commonv1.AssociationConf) { + kbent.entAssocConf = assocConf +} + +func (kbent *KibanaEntAssociation) AssociationID() string { + return commonv1.SingletonAssociationID } diff --git a/pkg/apis/kibana/v1/kibana_types_test.go b/pkg/apis/kibana/v1/kibana_types_test.go index 4e9e06b52d..c58f39cf52 100644 --- a/pkg/apis/kibana/v1/kibana_types_test.go +++ b/pkg/apis/kibana/v1/kibana_types_test.go @@ -11,5 +11,5 @@ import ( func TestApmEsAssociation_AssociationConfAnnotationName(t *testing.T) { k := Kibana{} - require.Equal(t, "association.k8s.elastic.co/es-conf", k.AssociationConfAnnotationName()) + require.Equal(t, "association.k8s.elastic.co/es-conf", k.EsAssociation().AssociationConfAnnotationName()) } diff --git a/pkg/apis/kibana/v1/zz_generated.deepcopy.go b/pkg/apis/kibana/v1/zz_generated.deepcopy.go index dae2dd70a1..ca7fb38fa2 100644 --- a/pkg/apis/kibana/v1/zz_generated.deepcopy.go +++ b/pkg/apis/kibana/v1/zz_generated.deepcopy.go @@ -25,6 +25,11 @@ func (in *Kibana) DeepCopyInto(out *Kibana) { *out = new(commonv1.AssociationConf) **out = **in } + if in.entAssocConf != nil { + in, out := &in.entAssocConf, &out.entAssocConf + *out = new(commonv1.AssociationConf) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kibana. @@ -45,6 +50,46 @@ func (in *Kibana) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KibanaEntAssociation) DeepCopyInto(out *KibanaEntAssociation) { + *out = *in + if in.Kibana != nil { + in, out := &in.Kibana, &out.Kibana + *out = new(Kibana) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KibanaEntAssociation. +func (in *KibanaEntAssociation) DeepCopy() *KibanaEntAssociation { + if in == nil { + return nil + } + out := new(KibanaEntAssociation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KibanaEsAssociation) DeepCopyInto(out *KibanaEsAssociation) { + *out = *in + if in.Kibana != nil { + in, out := &in.Kibana, &out.Kibana + *out = new(Kibana) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KibanaEsAssociation. +func (in *KibanaEsAssociation) DeepCopy() *KibanaEsAssociation { + if in == nil { + return nil + } + out := new(KibanaEsAssociation) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KibanaList) DeepCopyInto(out *KibanaList) { *out = *in @@ -81,6 +126,7 @@ func (in *KibanaList) DeepCopyObject() runtime.Object { func (in *KibanaSpec) DeepCopyInto(out *KibanaSpec) { *out = *in out.ElasticsearchRef = in.ElasticsearchRef + out.EnterpriseSearchRef = in.EnterpriseSearchRef if in.Config != nil { in, out := &in.Config, &out.Config *out = (*in).DeepCopy() diff --git a/pkg/controller/association/ca_test.go b/pkg/controller/association/ca_test.go index ee03c207a2..8aeea17a5a 100644 --- a/pkg/controller/association/ca_test.go +++ b/pkg/controller/association/ca_test.go @@ -75,7 +75,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { kibanaEsCA := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: es.Namespace, - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), }, Data: map[string][]byte{ certificates.CertFileName: []byte("fake-cert"), @@ -85,7 +85,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { updatedKibanaEsCA := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: es.Namespace, - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), }, Data: map[string][]byte{ certificates.CertFileName: []byte("updated-fake-cert"), @@ -105,7 +105,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { kibanaEmptyEsCA := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: es.Namespace, - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), }, Data: map[string][]byte{ certificates.CertFileName: []byte("fake-cert"), @@ -126,7 +126,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { client: k8s.NewFakeClient(&es, &esCA), kibana: kibanaFixture, es: esFixture, - want: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + want: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), wantCA: &kibanaEsCA, wantCACertProvided: true, }, @@ -135,7 +135,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { client: k8s.NewFakeClient(&es, &updatedEsCA, &kibanaEsCA), kibana: kibanaFixture, es: esFixture, - want: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + want: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), wantCA: &updatedKibanaEsCA, wantCACertProvided: true, }, @@ -154,7 +154,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { client: k8s.NewFakeClient(&es, &esEmptyCA), kibana: kibanaFixture, es: esFixture, - want: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + want: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), wantCA: &kibanaEmptyEsCA, wantCACertProvided: false, }, @@ -179,7 +179,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { caSecretServiceLabelName := "elasticsearch.k8s.elastic.co/cluster-name" got, err := r.ReconcileCASecret( - &tt.kibana, + tt.kibana.EsAssociation(), esv1.ESNamer, k8s.ExtractNamespacedName(&tt.es), ) @@ -192,7 +192,7 @@ func TestReconcileAssociation_reconcileCASecret(t *testing.T) { var updatedKibanaCA corev1.Secret err = tt.client.Get(context.Background(), types.NamespacedName{ Namespace: tt.kibana.Namespace, - Name: CACertSecretName(&kibanaFixture, "kibana-es"), + Name: CACertSecretName(kibanaFixture.EsAssociation(), "kibana-es"), }, &updatedKibanaCA) require.NoError(t, err) require.Equal(t, tt.wantCA.Data, updatedKibanaCA.Data) diff --git a/pkg/controller/association/conf_test.go b/pkg/controller/association/conf_test.go index 7f1e1af62e..e4c8901f40 100644 --- a/pkg/controller/association/conf_test.go +++ b/pkg/controller/association/conf_test.go @@ -156,7 +156,7 @@ func testFetchKibana(t *testing.T) { require.Equal(t, "kb-ns", got.Namespace) require.Equal(t, "test-image", got.Spec.Image) require.EqualValues(t, 1, got.Spec.Count) - require.Equal(t, tc.wantAssocConf, got.AssociationConf()) + require.Equal(t, tc.wantAssocConf, got.EsAssociation().AssociationConf()) }) } } @@ -175,7 +175,7 @@ func mkKibana(withAnnotations bool) *kbv1.Kibana { if withAnnotations { kb.ObjectMeta.Annotations = map[string]string{ - kb.AssociationConfAnnotationName(): `{"authSecretName":"auth-secret", "authSecretKey":"kb-user", "caSecretName": "ca-secret", "url":"https://es.svc:9300"}`, + kb.EsAssociation().AssociationConfAnnotationName(): `{"authSecretName":"auth-secret", "authSecretKey":"kb-user", "caSecretName": "ca-secret", "url":"https://es.svc:9300"}`, } kb.Spec.ElasticsearchRef = commonv1.ObjectSelector{ Name: "es-test", @@ -360,7 +360,7 @@ func TestUpdateAssociationConf(t *testing.T) { require.Equal(t, "kb-ns", got.Namespace) require.Equal(t, "test-image", got.Spec.Image) require.EqualValues(t, 1, got.Spec.Count) - require.Equal(t, assocConf, got.AssociationConf()) + require.Equal(t, assocConf, got.EsAssociation().AssociationConf()) // update and check the new values newAssocConf := &commonv1.AssociationConf{ @@ -370,7 +370,7 @@ func TestUpdateAssociationConf(t *testing.T) { URL: "https://new-es.svc:9300", } - err = UpdateAssociationConf(client, &got, newAssocConf) + err = UpdateAssociationConf(client, got.EsAssociation(), newAssocConf) require.NoError(t, err) err = FetchWithAssociations(context.Background(), client, request, &got) @@ -379,7 +379,7 @@ func TestUpdateAssociationConf(t *testing.T) { require.Equal(t, "kb-ns", got.Namespace) require.Equal(t, "test-image", got.Spec.Image) require.EqualValues(t, 1, got.Spec.Count) - require.Equal(t, newAssocConf, got.AssociationConf()) + require.Equal(t, newAssocConf, got.EsAssociation().AssociationConf()) } func TestRemoveAssociationConf(t *testing.T) { @@ -402,10 +402,10 @@ func TestRemoveAssociationConf(t *testing.T) { require.Equal(t, "kb-ns", got.Namespace) require.Equal(t, "test-image", got.Spec.Image) require.EqualValues(t, 1, got.Spec.Count) - require.Equal(t, assocConf, got.AssociationConf()) + require.Equal(t, assocConf, got.EsAssociation().AssociationConf()) // remove and check the new values - err = RemoveAssociationConf(client, &got) + err = RemoveAssociationConf(client, got.EsAssociation()) require.NoError(t, err) err = FetchWithAssociations(context.Background(), client, request, &got) @@ -414,7 +414,7 @@ func TestRemoveAssociationConf(t *testing.T) { require.Equal(t, "kb-ns", got.Namespace) require.Equal(t, "test-image", got.Spec.Image) require.EqualValues(t, 1, got.Spec.Count) - require.Nil(t, got.AssociationConf()) + require.Nil(t, got.EsAssociation().AssociationConf()) } func TestAllowVersion(t *testing.T) { diff --git a/pkg/controller/association/controller.go b/pkg/controller/association/controller.go index 0484179bcd..f6fdec400f 100644 --- a/pkg/controller/association/controller.go +++ b/pkg/controller/association/controller.go @@ -5,7 +5,6 @@ package association import ( - esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/common" "github.com/elastic/cloud-on-k8s/pkg/controller/common/operator" "github.com/elastic/cloud-on-k8s/pkg/controller/common/watches" @@ -48,29 +47,29 @@ func AddAssociationController( } func addWatches(c controller.Controller, r *Reconciler) error { - // Watch the associated resources + // Watch the associated resource (e.g. Kibana for a Kibana -> Elasticsearch association) if err := c.Watch(&source.Kind{Type: r.AssociatedObjTemplate()}, &handler.EnqueueRequestForObject{}); err != nil { return err } - // Watch Elasticsearch cluster objects - if err := c.Watch(&source.Kind{Type: &esv1.Elasticsearch{}}, r.watches.ElasticsearchClusters); err != nil { + // Watch Secrets owned by the associated resource + if err := c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForOwner{ + OwnerType: r.AssociatedObjTemplate(), + IsController: true, + }); err != nil { return err } - // Dynamically watch Elasticsearch public CA secrets for referenced ES clusters - if err := c.Watch(&source.Kind{Type: &corev1.Secret{}}, r.watches.Secrets); err != nil { + // Dynamically watch the referenced resources (e.g. Elasticsearch B for a Kibana A -> Elasticsearch B association) + if err := c.Watch(&source.Kind{Type: r.ReferencedObjTemplate()}, r.watches.ReferencedResources); err != nil { return err } - // Dynamically watch Service objects for custom services setup by the user - if err := c.Watch(&source.Kind{Type: &corev1.Service{}}, r.watches.Services); err != nil { + // Dynamically watch Secrets (CA Secret of the referenced resource and ES user secret) + if err := c.Watch(&source.Kind{Type: &corev1.Secret{}}, r.watches.Secrets); err != nil { return err } - // Watch Secrets owned by the associated resource - return c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForOwner{ - OwnerType: r.AssociatedObjTemplate(), - IsController: true, - }) + // Dynamically watch Service objects for custom services setup by the user + return c.Watch(&source.Kind{Type: &corev1.Service{}}, r.watches.Services) } diff --git a/pkg/controller/association/controller/agent_es.go b/pkg/controller/association/controller/agent_es.go index 1f596dd57b..34aae81b7a 100644 --- a/pkg/controller/association/controller/agent_es.go +++ b/pkg/controller/association/controller/agent_es.go @@ -7,6 +7,7 @@ package controller import ( agentv1alpha1 "github.com/elastic/cloud-on-k8s/pkg/apis/agent/v1alpha1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" @@ -19,24 +20,25 @@ import ( ) const ( - // AgentAssociationLabelName marks resources created by this controller for easier retrieval. + // AgentAssociationLabelName marks resources created for an association originating from Agent with the + // Agent name. AgentAssociationLabelName = "agentassociation.k8s.elastic.co/name" - // AgentAssociationLabelNamespace marks resources created by this controller for easier retrieval. + // AgentAssociationLabelNamespace marks resources created for an association originating from Agent with the + // Agent namespace. AgentAssociationLabelNamespace = "agentassociation.k8s.elastic.co/namespace" - // AgentAssociationLabelType marks the type of association + // AgentAssociationLabelType marks resources created for an association originating from Agent + // with the target resource type (e.g. "elasticsearch"). AgentAssociationLabelType = "agentassociation.k8s.elastic.co/type" ) func AddAgentES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ - AssociationType: commonv1.ElasticsearchAssociationType, - AssociatedObjTemplate: func() commonv1.Associated { return &agentv1alpha1.Agent{} }, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + AssociationType: commonv1.ElasticsearchAssociationType, + AssociatedObjTemplate: func() commonv1.Associated { return &agentv1alpha1.Agent{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, ReferencedResourceVersion: referencedElasticsearchStatusVersion, ExternalServiceURL: getElasticsearchExternalURL, - AssociatedNamer: esv1.ESNamer, + ReferencedResourceNamer: esv1.ESNamer, AssociationName: "agent-es", AssociatedShortName: "agent", Labels: func(associated types.NamespacedName) map[string]string { @@ -46,12 +48,18 @@ func AddAgentES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params AgentAssociationLabelType: commonv1.ElasticsearchAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "agent-user", - ESUserRole: func(associated commonv1.Associated) (string, error) { - return "superuser", nil - }, + AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "agent-user", + ESUserRole: func(associated commonv1.Associated) (string, error) { + return "superuser", nil + }, + }, }) } diff --git a/pkg/controller/association/controller/apm_es.go b/pkg/controller/association/controller/apm_es.go index fff431043d..3246418bf7 100644 --- a/pkg/controller/association/controller/apm_es.go +++ b/pkg/controller/association/controller/apm_es.go @@ -8,6 +8,8 @@ import ( "context" "strings" + "sigs.k8s.io/controller-runtime/pkg/client" + apmv1 "github.com/elastic/cloud-on-k8s/pkg/apis/apm/v1" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" @@ -25,25 +27,24 @@ import ( ) const ( - // ApmAssociationLabelName marks resources created for an association originating from APM. + // ApmAssociationLabelName marks resources created for an association originating from APM with the APM name. ApmAssociationLabelName = "apmassociation.k8s.elastic.co/name" - // ApmAssociationLabelNamespace marks resources created for an association originating from APM. + // ApmAssociationLabelNamespace marks resources created for an association originating from APM with the APM namespace. ApmAssociationLabelNamespace = "apmassociation.k8s.elastic.co/namespace" - // ApmAssociationLabelType marks resources created for an association originating from APM. + // ApmAssociationLabelType marks resources created for an association originating from APM with the target resource + // type (e.g. "elasticsearch" or "kibana"). ApmAssociationLabelType = "apmassociation.k8s.elastic.co/type" ) func AddApmES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ - AssociatedShortName: "apm", - AssociatedObjTemplate: func() commonv1.Associated { return &apmv1.ApmServer{} }, - AssociationType: commonv1.ElasticsearchAssociationType, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + AssociatedShortName: "apm", + AssociatedObjTemplate: func() commonv1.Associated { return &apmv1.ApmServer{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, + AssociationType: commonv1.ElasticsearchAssociationType, ReferencedResourceVersion: referencedElasticsearchStatusVersion, ExternalServiceURL: getElasticsearchExternalURL, - AssociatedNamer: esv1.ESNamer, + ReferencedResourceNamer: esv1.ESNamer, AssociationName: "apm-es", Labels: func(associated types.NamespacedName) map[string]string { return map[string]string{ @@ -53,10 +54,16 @@ func AddApmES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params op } }, AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "apm-user", - ESUserRole: getAPMElasticsearchRoles, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "apm-user", + ESUserRole: getAPMElasticsearchRoles, + }, }) } @@ -77,16 +84,6 @@ func getElasticsearchExternalURL(c k8s.Client, assoc commonv1.Association) (stri return association.ServiceURL(c, nsn, es.Spec.HTTP.Protocol()) } -// referencedElasticsearchStatusVersion returns the currently running version of Elasticsearch -// reported in its status. -func referencedElasticsearchStatusVersion(c k8s.Client, esRef types.NamespacedName) (string, error) { - var es esv1.Elasticsearch - if err := c.Get(context.Background(), esRef, &es); err != nil { - return "", err - } - return es.Status.Version, nil -} - // getAPMElasticsearchRoles returns for a given version of the APM Server the set of required roles. func getAPMElasticsearchRoles(associated commonv1.Associated) (string, error) { apmServer, ok := associated.(*apmv1.ApmServer) diff --git a/pkg/controller/association/controller/apm_kibana.go b/pkg/controller/association/controller/apm_kibana.go index 425822f4ad..de60a9c995 100644 --- a/pkg/controller/association/controller/apm_kibana.go +++ b/pkg/controller/association/controller/apm_kibana.go @@ -6,35 +6,30 @@ package controller import ( "context" - "fmt" apmv1 "github.com/elastic/cloud-on-k8s/pkg/apis/apm/v1" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/association" "github.com/elastic/cloud-on-k8s/pkg/controller/common/operator" - "github.com/elastic/cloud-on-k8s/pkg/controller/common/watches" "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/user" "github.com/elastic/cloud-on-k8s/pkg/controller/kibana" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/pkg/utils/rbac" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" ) -const ( - kibanaWatchNameTemplate = "%s-%s-kibana-watch" -) - func AddApmKibana(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ AssociatedShortName: "apm", AssociatedObjTemplate: func() commonv1.Associated { return &apmv1.ApmServer{} }, + ReferencedObjTemplate: func() client.Object { return &kbv1.Kibana{} }, ExternalServiceURL: getKibanaExternalURL, ReferencedResourceVersion: referencedKibanaStatusVersion, - ElasticsearchRef: getElasticsearchFromKibana, - AssociatedNamer: kibana.Namer, + ReferencedResourceNamer: kibana.Namer, AssociationName: "apm-kibana", AssociationType: commonv1.KibanaAssociationType, Labels: func(associated types.NamespacedName) map[string]string { @@ -44,27 +39,17 @@ func AddApmKibana(mgr manager.Manager, accessReviewer rbac.AccessReviewer, param ApmAssociationLabelType: commonv1.KibanaAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.KibanaConfigAnnotationNameBase, - UserSecretSuffix: "apm-kb-user", - ESUserRole: func(_ commonv1.Associated) (string, error) { - return user.ApmAgentUserRole, nil - }, - SetDynamicWatches: func(associated types.NamespacedName, associations []commonv1.Association, w watches.DynamicWatches) error { - return association.ReconcileWatch( - associated, - associations, - w.Kibanas, - fmt.Sprintf(kibanaWatchNameTemplate, associated.Namespace, associated.Name), - func(association commonv1.Association) types.NamespacedName { - return association.AssociationRef().NamespacedName() - }, - ) - }, - ClearDynamicWatches: func(associated types.NamespacedName, w watches.DynamicWatches) { - association.RemoveWatch(w.Kibanas, fmt.Sprintf(kibanaWatchNameTemplate, associated.Namespace, associated.Name)) - }, + AssociationConfAnnotationNameBase: commonv1.KibanaConfigAnnotationNameBase, AssociationResourceNameLabelName: kibana.KibanaNameLabelName, AssociationResourceNamespaceLabelName: kibana.KibanaNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: getElasticsearchFromKibana, + UserSecretSuffix: "apm-kb-user", + ESUserRole: func(_ commonv1.Associated) (string, error) { + return user.ApmAgentUserRole, nil + }, + }, }) } @@ -89,7 +74,8 @@ func getKibanaExternalURL(c k8s.Client, assoc commonv1.Association) (string, err // reported in its status. func referencedKibanaStatusVersion(c k8s.Client, kbRef types.NamespacedName) (string, error) { var kb kbv1.Kibana - if err := c.Get(context.Background(), kbRef, &kb); err != nil { + err := c.Get(context.Background(), kbRef, &kb) + if err != nil { return "", err } return kb.Status.Version, nil @@ -111,5 +97,5 @@ func getElasticsearchFromKibana(c k8s.Client, association commonv1.Association) return false, commonv1.ObjectSelector{}, err } - return true, kb.AssociationRef(), nil + return true, kb.EsAssociation().AssociationRef(), nil } diff --git a/pkg/controller/association/controller/beat_es.go b/pkg/controller/association/controller/beat_es.go index bfa5ba2378..568584b1ae 100644 --- a/pkg/controller/association/controller/beat_es.go +++ b/pkg/controller/association/controller/beat_es.go @@ -8,6 +8,8 @@ import ( "fmt" "strings" + "sigs.k8s.io/controller-runtime/pkg/client" + pkgerrors "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -25,24 +27,25 @@ import ( ) const ( - // BeatAssociationLabelName marks resources created by this controller for easier retrieval. + // BeatAssociationLabelName marks resources created for an association originating from Beat with the + // Beat name. BeatAssociationLabelName = "beatassociation.k8s.elastic.co/name" - // BeatAssociationLabelNamespace marks resources created by this controller for easier retrieval. + // BeatAssociationLabelNamespace marks resources created for an association originating from Beat with the + // Beat namespace. BeatAssociationLabelNamespace = "beatassociation.k8s.elastic.co/namespace" - // BeatAssociationLabelType marks the type of association + // AgentAssociationLabelType marks resources created for an association originating from Beat + // with the target resource type (e.g. "elasticsearch" or "kibana"). BeatAssociationLabelType = "beatassociation.k8s.elastic.co/type" ) func AddBeatES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ - AssociatedObjTemplate: func() commonv1.Associated { return &beatv1beta1.Beat{} }, - AssociationType: commonv1.ElasticsearchAssociationType, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + AssociatedObjTemplate: func() commonv1.Associated { return &beatv1beta1.Beat{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, + AssociationType: commonv1.ElasticsearchAssociationType, ReferencedResourceVersion: referencedElasticsearchStatusVersion, ExternalServiceURL: getElasticsearchExternalURL, - AssociatedNamer: esv1.ESNamer, + ReferencedResourceNamer: esv1.ESNamer, AssociationName: "beat-es", AssociatedShortName: "beat", Labels: func(associated types.NamespacedName) map[string]string { @@ -53,10 +56,16 @@ func AddBeatES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params o } }, AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "beat-user", - ESUserRole: getBeatRoles, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "beat-user", + ESUserRole: getBeatRoles, + }, }) } diff --git a/pkg/controller/association/controller/beat_kibana.go b/pkg/controller/association/controller/beat_kibana.go index 8279dccca8..75194276ea 100644 --- a/pkg/controller/association/controller/beat_kibana.go +++ b/pkg/controller/association/controller/beat_kibana.go @@ -10,29 +10,26 @@ import ( beatv1beta1 "github.com/elastic/cloud-on-k8s/pkg/apis/beat/v1beta1" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" + kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/association" "github.com/elastic/cloud-on-k8s/pkg/controller/common/operator" "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" - "github.com/elastic/cloud-on-k8s/pkg/controller/common/watches" esuser "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/user" "github.com/elastic/cloud-on-k8s/pkg/controller/kibana" "github.com/elastic/cloud-on-k8s/pkg/utils/rbac" pkgerrors "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" ) -const ( - beatWatchNameTemplate = "%s-%s-beat-watch" -) - func AddBeatKibana(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ AssociatedObjTemplate: func() commonv1.Associated { return &beatv1beta1.Beat{} }, - ElasticsearchRef: getElasticsearchFromKibana, + ReferencedObjTemplate: func() client.Object { return &kbv1.Kibana{} }, ExternalServiceURL: getKibanaExternalURL, ReferencedResourceVersion: referencedKibanaStatusVersion, - AssociatedNamer: kibana.Namer, + ReferencedResourceNamer: kibana.Namer, AssociationName: "beat-kibana", AssociatedShortName: "beat", AssociationType: commonv1.KibanaAssociationType, @@ -43,27 +40,15 @@ func AddBeatKibana(mgr manager.Manager, accessReviewer rbac.AccessReviewer, para BeatAssociationLabelType: commonv1.KibanaAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.KibanaConfigAnnotationNameBase, - UserSecretSuffix: "beat-kb-user", - ESUserRole: getBeatKibanaRoles, - // The generic association controller watches Elasticsearch by default but we are interested in changes to - // Kibana as well for the purposes of establishing the association. - SetDynamicWatches: func(associated types.NamespacedName, associations []commonv1.Association, w watches.DynamicWatches) error { - return association.ReconcileWatch( - associated, - associations, - w.Kibanas, - fmt.Sprintf(beatWatchNameTemplate, associated.Namespace, associated.Name), - func(association commonv1.Association) types.NamespacedName { - return association.AssociationRef().NamespacedName() - }, - ) - }, - ClearDynamicWatches: func(associated types.NamespacedName, w watches.DynamicWatches) { - association.RemoveWatch(w.Kibanas, fmt.Sprintf(beatWatchNameTemplate, associated.Namespace, associated.Name)) - }, + AssociationConfAnnotationNameBase: commonv1.KibanaConfigAnnotationNameBase, AssociationResourceNameLabelName: kibana.KibanaNameLabelName, AssociationResourceNamespaceLabelName: kibana.KibanaNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: getElasticsearchFromKibana, + UserSecretSuffix: "beat-kb-user", + ESUserRole: getBeatKibanaRoles, + }, }) } diff --git a/pkg/controller/association/controller/ent_es.go b/pkg/controller/association/controller/ent_es.go index 6706f2de70..662f86d59c 100644 --- a/pkg/controller/association/controller/ent_es.go +++ b/pkg/controller/association/controller/ent_es.go @@ -6,6 +6,7 @@ package controller import ( "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" @@ -20,24 +21,25 @@ import ( ) const ( - // EntESAssociationLabelName marks resources created by this controller for easier retrieval. + // EntESAssociationLabelName marks resources created for an association originating from EnterpriseSearch with the + // EnterpriseSearch name. EntESAssociationLabelName = "entassociation.k8s.elastic.co/name" - // EntESAssociationLabelNamespace marks resources created by this controller for easier retrieval. + // EntESAssociationLabelNamespace marks resources created for an association originating from EnterpriseSearch with the + // EnterpriseSearch namespace. EntESAssociationLabelNamespace = "entassociation.k8s.elastic.co/namespace" - // EntESAssociationLabelType marks resources created for an association originating from Enterprise Search. + // EntESAssociationLabelType marks resources created for an association originating from EnterpriseSearch + // with the target resource type (e.g. "elasticsearch"). EntESAssociationLabelType = "entassociation.k8s.elastic.co/type" ) func AddEntES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ - AssociatedObjTemplate: func() commonv1.Associated { return &entv1.EnterpriseSearch{} }, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + AssociatedObjTemplate: func() commonv1.Associated { return &entv1.EnterpriseSearch{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, ReferencedResourceVersion: referencedElasticsearchStatusVersion, AssociationType: commonv1.ElasticsearchAssociationType, ExternalServiceURL: getElasticsearchExternalURL, - AssociatedNamer: esv1.ESNamer, + ReferencedResourceNamer: esv1.ESNamer, AssociationName: "ent-es", AssociatedShortName: "ent", Labels: func(associated types.NamespacedName) map[string]string { @@ -47,12 +49,18 @@ func AddEntES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params op EntESAssociationLabelType: commonv1.ElasticsearchAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "ent-user", - ESUserRole: func(_ commonv1.Associated) (string, error) { - return esuser.SuperUserBuiltinRole, nil - }, + AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "ent-user", + ESUserRole: func(_ commonv1.Associated) (string, error) { + return esuser.SuperUserBuiltinRole, nil + }, + }, }) } diff --git a/pkg/controller/association/controller/kibana_ent.go b/pkg/controller/association/controller/kibana_ent.go new file mode 100644 index 0000000000..eabe1bb380 --- /dev/null +++ b/pkg/controller/association/controller/kibana_ent.go @@ -0,0 +1,73 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package controller + +import ( + "context" + + commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" + entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" + kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" + "github.com/elastic/cloud-on-k8s/pkg/controller/association" + "github.com/elastic/cloud-on-k8s/pkg/controller/common/operator" + entctl "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch" + "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" + "github.com/elastic/cloud-on-k8s/pkg/utils/rbac" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +func AddKibanaEnt(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { + return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ + AssociatedObjTemplate: func() commonv1.Associated { return &kbv1.Kibana{} }, + ReferencedObjTemplate: func() client.Object { return &entv1.EnterpriseSearch{} }, + ExternalServiceURL: getEntExternalURL, + ReferencedResourceVersion: referencedEntStatusVersion, + ReferencedResourceNamer: entv1.Namer, + AssociationName: "kb-ent", + AssociatedShortName: "kb", + AssociationType: commonv1.EntAssociationType, + Labels: func(associated types.NamespacedName) map[string]string { + return map[string]string{ + KibanaAssociationLabelName: associated.Name, + KibanaAssociationLabelNamespace: associated.Namespace, + KibanaAssociationLabelType: commonv1.EntAssociationType, + } + }, + AssociationConfAnnotationNameBase: commonv1.EntConfigAnnotationNameBase, + AssociationResourceNameLabelName: entctl.EnterpriseSearchNameLabelName, + AssociationResourceNamespaceLabelName: entctl.EnterpriseSearchNamespaceLabelName, + ElasticsearchUserCreation: nil, // no dedicated ES user required for Kibana->Ent connection + }) +} + +func getEntExternalURL(c k8s.Client, assoc commonv1.Association) (string, error) { + entRef := assoc.AssociationRef() + if !entRef.IsDefined() { + return "", nil + } + ent := entv1.EnterpriseSearch{} + if err := c.Get(context.Background(), entRef.NamespacedName(), &ent); err != nil { + return "", err + } + serviceName := entRef.ServiceName + if serviceName == "" { + serviceName = entctl.HTTPServiceName(ent.Name) + } + nsn := types.NamespacedName{Namespace: ent.Namespace, Name: serviceName} + return association.ServiceURL(c, nsn, ent.Spec.HTTP.Protocol()) +} + +// referencedEntStatusVersion returns the currently running version of Enterprise Search +// reported in its status. +func referencedEntStatusVersion(c k8s.Client, entRef types.NamespacedName) (string, error) { + var ent entv1.EnterpriseSearch + err := c.Get(context.Background(), entRef, &ent) + if err != nil { + return "", err + } + return ent.Status.Version, nil +} diff --git a/pkg/controller/association/controller/kibana_es.go b/pkg/controller/association/controller/kibana_es.go index 9d4d9c727b..a2c0723fe4 100644 --- a/pkg/controller/association/controller/kibana_es.go +++ b/pkg/controller/association/controller/kibana_es.go @@ -5,8 +5,7 @@ package controller import ( - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/manager" + "context" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" @@ -16,15 +15,21 @@ import ( eslabel "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/label" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/pkg/utils/rbac" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" ) const ( - // KibanaESAssociationLabelName marks resources created by this controller for easier retrieval. - KibanaESAssociationLabelName = "kibanaassociation.k8s.elastic.co/name" - // KibanaESAssociationLabelNamespace marks resources created by this controller for easier retrieval. - KibanaESAssociationLabelNamespace = "kibanaassociation.k8s.elastic.co/namespace" - // KibanaESAssociationLabelType marks the type of association - KibanaESAssociationLabelType = "kibanaassociation.k8s.elastic.co/type" + // KibanaAssociationLabelName marks resources created for an association originating from Kibana with the + // Kibana name. + KibanaAssociationLabelName = "kibanaassociation.k8s.elastic.co/name" + // KibanaAssociationLabelNamespace marks resources created for an association originating from Kibana with the + // Kibana namespace. + KibanaAssociationLabelNamespace = "kibanaassociation.k8s.elastic.co/namespace" + // KibanaAssociationLabelType marks resources created for an association originating from Kibana + // with the target resource type (e.g. "elasticsearch" or "ent). + KibanaAssociationLabelType = "kibanaassociation.k8s.elastic.co/type" // KibanaSystemUserBuiltinRole is the name of the built-in role for the Kibana system user. KibanaSystemUserBuiltinRole = "kibana_system" @@ -32,29 +37,44 @@ const ( func AddKibanaES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ - AssociatedObjTemplate: func() commonv1.Associated { return &kbv1.Kibana{} }, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + AssociatedObjTemplate: func() commonv1.Associated { return &kbv1.Kibana{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, ReferencedResourceVersion: referencedElasticsearchStatusVersion, ExternalServiceURL: getElasticsearchExternalURL, AssociationType: commonv1.ElasticsearchAssociationType, - AssociatedNamer: esv1.ESNamer, + ReferencedResourceNamer: esv1.ESNamer, AssociationName: "kb-es", AssociatedShortName: "kb", Labels: func(associated types.NamespacedName) map[string]string { return map[string]string{ - KibanaESAssociationLabelName: associated.Name, - KibanaESAssociationLabelNamespace: associated.Namespace, - KibanaESAssociationLabelType: commonv1.ElasticsearchAssociationType, + KibanaAssociationLabelName: associated.Name, + KibanaAssociationLabelNamespace: associated.Namespace, + KibanaAssociationLabelType: commonv1.ElasticsearchAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "kibana-user", - ESUserRole: func(associated commonv1.Associated) (string, error) { - return KibanaSystemUserBuiltinRole, nil - }, + AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "kibana-user", + ESUserRole: func(associated commonv1.Associated) (string, error) { + return KibanaSystemUserBuiltinRole, nil + }, + }, }) } + +// referencedElasticsearchStatusVersion returns the currently running version of Elasticsearch +// reported in its status. +func referencedElasticsearchStatusVersion(c k8s.Client, esRef types.NamespacedName) (string, error) { + var es esv1.Elasticsearch + err := c.Get(context.Background(), esRef, &es) + if err != nil { + return "", err + } + return es.Status.Version, nil +} diff --git a/pkg/controller/association/controller/maps_es.go b/pkg/controller/association/controller/maps_es.go index 4c8a15ba04..596594e694 100644 --- a/pkg/controller/association/controller/maps_es.go +++ b/pkg/controller/association/controller/maps_es.go @@ -15,15 +15,19 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/pkg/utils/rbac" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" ) const ( - // MapsESAssociationLabelName marks resources created by this controller for easier retrieval. + // MapsESAssociationLabelName marks resources created for an association originating from Maps with the + // Maps name. MapsESAssociationLabelName = "mapsassociation.k8s.elastic.co/name" - // MapsESAssociationLabelNamespace marks resources created by this controller for easier retrieval. + // MapsESAssociationLabelNamespace marks resources created for an association originating from Maps with the + // Maps namespace. MapsESAssociationLabelNamespace = "mapsassociation.k8s.elastic.co/namespace" - // MapsESAssociationLabelType marks the type of association + // MapsESAssociationLabelType marks resources created for an association originating from Maps + // with the target resource type (e.g. "elasticsearch"). MapsESAssociationLabelType = "mapsassociation.k8s.elastic.co/type" // MapsSystemUserBuiltinRole is the name of the built-in role for the Maps system user. @@ -32,14 +36,12 @@ const ( func AddMapsES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params operator.Parameters) error { return association.AddAssociationController(mgr, accessReviewer, params, association.AssociationInfo{ - AssociatedObjTemplate: func() commonv1.Associated { return &emsv1alpha1.ElasticMapsServer{} }, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + AssociatedObjTemplate: func() commonv1.Associated { return &emsv1alpha1.ElasticMapsServer{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, ReferencedResourceVersion: referencedElasticsearchStatusVersion, ExternalServiceURL: getElasticsearchExternalURL, AssociationType: commonv1.ElasticsearchAssociationType, - AssociatedNamer: esv1.ESNamer, + ReferencedResourceNamer: esv1.ESNamer, AssociationName: "ems-es", AssociatedShortName: "ems", Labels: func(associated types.NamespacedName) map[string]string { @@ -49,12 +51,18 @@ func AddMapsES(mgr manager.Manager, accessReviewer rbac.AccessReviewer, params o MapsESAssociationLabelType: commonv1.ElasticsearchAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "maps-user", - ESUserRole: func(associated commonv1.Associated) (string, error) { - return MapsSystemUserBuiltinRole, nil - }, + AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + + ElasticsearchUserCreation: &association.ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "maps-user", + ESUserRole: func(associated commonv1.Associated) (string, error) { + return MapsSystemUserBuiltinRole, nil + }, + }, }) } diff --git a/pkg/controller/association/dynamic_watches.go b/pkg/controller/association/dynamic_watches.go index 2c2b8c0971..73a17fd1ba 100644 --- a/pkg/controller/association/dynamic_watches.go +++ b/pkg/controller/association/dynamic_watches.go @@ -13,9 +13,14 @@ import ( "k8s.io/apimachinery/pkg/types" ) -// esWatchName returns the name of the watch setup on the referenced Elasticsearch resource. -func esWatchName(associated types.NamespacedName) string { - return fmt.Sprintf("%s-%s-es-watch", associated.Namespace, associated.Name) +// referencedResourceWatchName is the name of the watch set on the referenced resource. +func referencedResourceWatchName(associated types.NamespacedName) string { + return fmt.Sprintf("%s-%s-referenced-resource-watch", associated.Namespace, associated.Name) +} + +// referencedResourceWatchName is the name of the watch set on Secret containing the CA of the referenced resource. +func referencedResourceCASecretWatchName(associated types.NamespacedName) string { + return fmt.Sprintf("%s-%s-referenced-resource-ca-secret-watch", associated.Namespace, associated.Name) } // esUserWatchName returns the name of the watch setup on the ES user secret. @@ -23,45 +28,32 @@ func esUserWatchName(associated types.NamespacedName) string { return fmt.Sprintf("%s-%s-es-user-watch", associated.Namespace, associated.Name) } -// associatedCAWatchName returns the name of the watch setup on the secret of the associated resource that -// contains the HTTP certificate chain of Elasticsearch. -func associatedCAWatchName(associated types.NamespacedName) string { - return fmt.Sprintf("%s-%s-ca-watch", associated.Namespace, associated.Name) -} - -// serviceWatchName returns the name of the watch monitor a custom service to be used to make requests to the -// associated resource. +// serviceWatchName returns the name of the watch setup on the custom service to be used to make requests to the +// referenced resource. func serviceWatchName(associated types.NamespacedName) string { return fmt.Sprintf("%s-%s-svc-watch", associated.Namespace, associated.Name) } -// reconcileWatches sets up dynamic watches related to: -// * The referenced Elasticsearch resource -// * The user created in the Elasticsearch namespace -// * The CA of the target service (can be Kibana or Elasticsearch in the case of the APM) -// All watches for all Associations are set under the same watch name for associated resource and replaced -// with each reconciliation. +// reconcileWatches sets up dynamic watches for: +// * the referenced resource(s) (e.g. Elasticsearch for Kibana -> Elasticsearch associations) +// * the CA secret of the referenced resource in the referenced resource namespace +// * the referenced service to access the referenced resource +// * if there's an ES user to create, watch the user Secret in ES namespace +// All watches for all given associations are set under the same watch name and replaced with each reconciliation. +// The given associations are expected to be of the same type (e.g. Kibana -> Elasticsearch, not Kibana -> Enterprise Search). func (r *Reconciler) reconcileWatches(associated types.NamespacedName, associations []commonv1.Association) error { - // watch the referenced ES cluster for future reconciliations - if err := ReconcileWatch(associated, associations, r.watches.ElasticsearchClusters, esWatchName(associated), func(association commonv1.Association) types.NamespacedName { + // watch the referenced resource + if err := ReconcileWatch(associated, associations, r.watches.ReferencedResources, referencedResourceWatchName(associated), func(association commonv1.Association) types.NamespacedName { return association.AssociationRef().NamespacedName() }); err != nil { return err } - // watch the user secret in the ES namespace - if err := ReconcileWatch(associated, associations, r.watches.Secrets, esUserWatchName(associated), func(association commonv1.Association) types.NamespacedName { - return UserKey(association, association.AssociationRef().Namespace, r.UserSecretSuffix) - }); err != nil { - return err - } - - // watch the CA secret in the targeted service namespace - // Most of the time it is Elasticsearch, but it could be Kibana in the case of the APMServer - if err := ReconcileWatch(associated, associations, r.watches.Secrets, associatedCAWatchName(associated), func(association commonv1.Association) types.NamespacedName { + // watch the CA secret of the referenced resource in the referenced resource namespace + if err := ReconcileWatch(associated, associations, r.watches.Secrets, referencedResourceCASecretWatchName(associated), func(association commonv1.Association) types.NamespacedName { ref := association.AssociationRef() return types.NamespacedName{ - Name: certificates.PublicCertsSecretName(r.AssociationInfo.AssociatedNamer, ref.Name), + Name: certificates.PublicCertsSecretName(r.AssociationInfo.ReferencedResourceNamer, ref.Name), Namespace: ref.Namespace, } }); err != nil { @@ -80,9 +72,11 @@ func (r *Reconciler) reconcileWatches(associated types.NamespacedName, associati return err } - // set additional watches, in the case of a transitive Elasticsearch reference we must watch the intermediate resource - if r.SetDynamicWatches != nil { - if err := r.SetDynamicWatches(associated, associations, r.watches); err != nil { + // watch the Elasticsearch user secret in the Elasticsearch namespace, if needed + if r.ElasticsearchUserCreation != nil { + if err := ReconcileWatch(associated, associations, r.watches.Secrets, esUserWatchName(associated), func(association commonv1.Association) types.NamespacedName { + return UserKey(association, association.AssociationRef().Namespace, r.ElasticsearchUserCreation.UserSecretSuffix) + }); err != nil { return err } } @@ -91,7 +85,7 @@ func (r *Reconciler) reconcileWatches(associated types.NamespacedName, associati } // ReconcileWatch sets or removes `watchName` watch in `dynamicRequest` based on `associated` and `associations` and -// `watchedFunc`. +// `watchedFunc`. No watch is added if watchedFunc(association) refers to an empty namespaced name. func ReconcileWatch( associated types.NamespacedName, associations []commonv1.Association, @@ -105,9 +99,14 @@ func ReconcileWatch( return nil } + emptyNamespacedName := types.NamespacedName{} + toWatch := make([]types.NamespacedName, 0, len(associations)) for _, association := range associations { - toWatch = append(toWatch, watchedFunc(association)) + watchedNamespacedName := watchedFunc(association) + if watchedNamespacedName != emptyNamespacedName { + toWatch = append(toWatch, watchedFunc(association)) + } } return dynamicRequest.AddHandler(watches.NamedWatch{ @@ -123,12 +122,12 @@ func RemoveWatch(dynamicRequest *watches.DynamicEnqueueRequest, watchName string } func (r *Reconciler) removeWatches(associated types.NamespacedName) { - // - ES resource - RemoveWatch(r.watches.ElasticsearchClusters, esWatchName(associated)) - // - user in the ES namespace - RemoveWatch(r.watches.Secrets, esUserWatchName(associated)) - // - ES CA Secret in the ES namespace - RemoveWatch(r.watches.Secrets, associatedCAWatchName(associated)) + // - referenced resource + RemoveWatch(r.watches.ReferencedResources, referencedResourceWatchName(associated)) + // - CA secret in referenced resource namespace + RemoveWatch(r.watches.Secrets, referencedResourceCASecretWatchName(associated)) // - custom service watch in resource namespace RemoveWatch(r.watches.Services, serviceWatchName(associated)) + // - ES user secret + RemoveWatch(r.watches.Secrets, esUserWatchName(associated)) } diff --git a/pkg/controller/association/reconciler.go b/pkg/controller/association/reconciler.go index 2d2d826f05..dae1d94438 100644 --- a/pkg/controller/association/reconciler.go +++ b/pkg/controller/association/reconciler.go @@ -45,13 +45,13 @@ type AssociationInfo struct { // AssociationType identifies the type of the resource for association (eg. kibana for APM to Kibana association, // elasticsearch for Beat to Elasticsearch association) AssociationType commonv1.AssociationType - // AssociatedObjTemplate builds an empty typed associated object (eg. &Kibana{} for Kibana to Elasticsearch association). + // AssociatedObjTemplate builds an empty typed associated object (eg. &Kibana{} for a Kibana to Elasticsearch association). AssociatedObjTemplate func() commonv1.Associated - // ElasticsearchRef is a function which returns the maybe transitive Elasticsearch reference (eg. APMServer -> Kibana -> Elasticsearch). - // In the case of a transitive reference this is used to create the Elasticsearch user. - ElasticsearchRef func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) - // AssociatedNamer is used to build the name of the Secret which contains the CA of the target. - AssociatedNamer name.Namer + // ReferencedObjTemplate builds an empty referenced object (e.g. Elasticsearch{} for a Kibana to Elasticsearch association). + ReferencedObjTemplate func() client.Object + // ReferencedResourceNamer is used to build the name of the Secret which contains the CA of the referenced resource + // (the Elasticsearch Namer for a Kibana to Elasticsearch association). + ReferencedResourceNamer name.Namer // ExternalServiceURL is used to build the external service url as it will be set in the resource configuration. ExternalServiceURL func(c k8s.Client, association commonv1.Association) (string, error) // AssociationName is the name of the association (eg. "kb-es"). @@ -67,14 +67,6 @@ type AssociationInfo struct { // the controller which is managing the associated resource to build the appropriate configuration. The annotation // base is used to recognize annotations eligible for removal when association is removed. AssociationConfAnnotationNameBase string - // UserSecretSuffix is used as a suffix in the name of the secret holding user data in the associated namespace. - UserSecretSuffix string - // ESUserRole is the role to use for the Elasticsearch user created by the association. - ESUserRole func(commonv1.Associated) (string, error) - // SetDynamicWatches allows to set some specific watches. - SetDynamicWatches func(associated types.NamespacedName, associations []commonv1.Association, watches watches.DynamicWatches) error - // ClearDynamicWatches is called when the controller needs to clear the specific watches set for the associated resource. - ClearDynamicWatches func(associated types.NamespacedName, watches watches.DynamicWatches) // ReferencedResourceVersion returns the currently running version of the referenced resource. // It may return an empty string if the version is unknown. ReferencedResourceVersion func(c k8s.Client, referencedRes types.NamespacedName) (string, error) @@ -86,6 +78,20 @@ type AssociationInfo struct { // namespace of the associated resource (eg. user secret allowing to connect Beat to Kibana will have this label // pointing to the Beat resource). AssociationResourceNamespaceLabelName string + + // ElasticsearchUserCreation specifies settings to create an Elasticsearch user as part of the association. + // May be nil if no user creation is required. + ElasticsearchUserCreation *ElasticsearchUserCreation +} + +type ElasticsearchUserCreation struct { + // ElasticsearchRef is a function which returns the maybe transitive Elasticsearch reference (eg. APMServer -> Kibana -> Elasticsearch). + // In the case of a transitive reference this is used to create the Elasticsearch user. + ElasticsearchRef func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) + // UserSecretSuffix is used as a suffix in the name of the secret holding user data in the associated namespace. + UserSecretSuffix string + // ESUserRole is the role to use for the Elasticsearch user created by the association. + ESUserRole func(commonv1.Associated) (string, error) } // AssociationResourceLabels returns all labels required by a resource to allow identifying both its Associated resource @@ -175,17 +181,25 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( return reconcile.Result{}, tracing.CaptureError(ctx, err) } - associations := associated.GetAssociations() + if err := RemoveObsoleteAssociationConfs(r.Client, associated, r.AssociationConfAnnotationNameBase); err != nil { + return reconcile.Result{}, tracing.CaptureError(ctx, err) + } + + // we are only interested in associations of the same target type here + // (e.g. Kibana -> Enterprise Search, not Kibana -> Elasticsearch) + associations := make([]commonv1.Association, 0) + for _, association := range associated.GetAssociations() { + if association.AssociationType() == r.AssociationType { + associations = append(associations, association) + } + } // garbage collect leftover resources that are not required anymore if err := deleteOrphanedResources(ctx, r.Client, r.AssociationInfo, associatedKey, associations); err != nil { r.log(associatedKey).Error(err, "Error while trying to delete orphaned resources. Continuing.") } - if err := RemoveObsoleteAssociationConfs(r.Client, associated, r.AssociationConfAnnotationNameBase); err != nil { - return reconcile.Result{}, tracing.CaptureError(ctx, err) - } - + // reconcile watches for all associations of this type if err := r.reconcileWatches(associatedKey, associations); err != nil { return reconcile.Result{}, tracing.CaptureError(ctx, err) } @@ -193,12 +207,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( results := reconciler.NewResult(ctx) newStatusMap := commonv1.AssociationStatusMap{} for _, association := range associations { - if association.AssociationType() != r.AssociationType { - // some resources have more than one type of resource associations, making sure we are looking at the right - // one for this controller - continue - } - newStatus, err := r.reconcileAssociation(ctx, association) if err != nil { results.WithError(err) @@ -218,52 +226,21 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( } func (r *Reconciler) reconcileAssociation(ctx context.Context, association commonv1.Association) (commonv1.AssociationStatus, error) { - // retrieve the Elasticsearch resource, since it can be a transitive reference we need to use the provided ElasticsearchRef function - associatedResourceFound, esRef, err := r.ElasticsearchRef(r.Client, association) + exists, err := k8s.ObjectExists(r.Client, association.AssociationRef().NamespacedName(), r.ReferencedObjTemplate()) if err != nil { return commonv1.AssociationFailed, err } - - // the associated resource does not exist yet, set status to Pending - if !associatedResourceFound { + if !exists { + // the associated resource does not exist (yet), set status to Pending and remove the existing association conf return commonv1.AssociationPending, RemoveAssociationConf(r.Client, association) } - es, associationStatus, err := r.getElasticsearch(ctx, association, esRef) - if associationStatus != "" || err != nil { - return associationStatus, err - } - - // from this point we have checked that all the associated resources are set and have been found. - - // check if reference to Elasticsearch is allowed to be established - if allowed, err := CheckAndUnbind(ctx, r.accessReviewer, association, &es, r, r.recorder); err != nil || !allowed { - return commonv1.AssociationPending, err - } - associationRef := association.AssociationRef() - - userRole, err := r.ESUserRole(association.Associated()) - if err != nil { - return commonv1.AssociationFailed, err - } - assocLabels := r.AssociationResourceLabels(k8s.ExtractNamespacedName(association.Associated()), association.AssociationRef().NamespacedName()) - if err := ReconcileEsUser( - ctx, - r.Client, - association, - assocLabels, - userRole, - r.UserSecretSuffix, - es, - ); err != nil { - return commonv1.AssociationPending, err - } caSecret, err := r.ReconcileCASecret( association, - r.AssociationInfo.AssociatedNamer, + r.AssociationInfo.ReferencedResourceNamer, associationRef.NamespacedName(), ) if err != nil { @@ -283,16 +260,61 @@ func (r *Reconciler) reconcileAssociation(ctx context.Context, association commo } // construct the expected association configuration - authSecretRef := UserSecretKeySelector(association, r.UserSecretSuffix) expectedAssocConf := &commonv1.AssociationConf{ - AuthSecretName: authSecretRef.Name, - AuthSecretKey: authSecretRef.Key, CACertProvided: caSecret.CACertProvided, CASecretName: caSecret.Name, URL: url, Version: ver, } + if r.ElasticsearchUserCreation == nil { + // no user creation required, update the association conf as such + expectedAssocConf.AuthSecretName = commonv1.NoAuthRequiredValue + return r.updateAssocConf(ctx, expectedAssocConf, association) + } + + // retrieve the Elasticsearch resource + // since it can be a transitive reference we need to use the provided ElasticsearchRef function + found, esRef, err := r.ElasticsearchUserCreation.ElasticsearchRef(r.Client, association) + if err != nil { + return commonv1.AssociationFailed, err + } + // the Elasticsearch resource does not exist yet, set status to Pending + if !found { + return commonv1.AssociationPending, RemoveAssociationConf(r.Client, association) + } + + es, associationStatus, err := r.getElasticsearch(ctx, association, esRef) + if associationStatus != "" || err != nil { + return associationStatus, err + } + + // check if reference to Elasticsearch is allowed to be established + if allowed, err := CheckAndUnbind(ctx, r.accessReviewer, association, &es, r, r.recorder); err != nil || !allowed { + return commonv1.AssociationPending, err + } + + userRole, err := r.ElasticsearchUserCreation.ESUserRole(association.Associated()) + if err != nil { + return commonv1.AssociationFailed, err + } + + if err := reconcileEsUserSecret( + ctx, + r.Client, + association, + assocLabels, + userRole, + r.ElasticsearchUserCreation.UserSecretSuffix, + es, + ); err != nil { + return commonv1.AssociationPending, err + } + + authSecretRef := UserSecretKeySelector(association, r.ElasticsearchUserCreation.UserSecretSuffix) + expectedAssocConf.AuthSecretName = authSecretRef.Name + expectedAssocConf.AuthSecretKey = authSecretRef.Key + // update the association configuration if necessary return r.updateAssocConf(ctx, expectedAssocConf, association) } @@ -416,11 +438,7 @@ func resultFromStatuses(statusMap commonv1.AssociationStatusMap) reconcile.Resul } func (r *Reconciler) onDelete(ctx context.Context, associated types.NamespacedName) { - // remove dynamic watches - if r.SetDynamicWatches != nil { - r.ClearDynamicWatches(associated, r.watches) - } - // remove other watches + // remove watches r.removeWatches(associated) // delete user Secret in the Elasticsearch namespace diff --git a/pkg/controller/association/reconciler_test.go b/pkg/controller/association/reconciler_test.go index a84c6744ac..9e8176e47a 100644 --- a/pkg/controller/association/reconciler_test.go +++ b/pkg/controller/association/reconciler_test.go @@ -14,6 +14,7 @@ import ( agentv1alpha1 "github.com/elastic/cloud-on-k8s/pkg/apis/agent/v1alpha1" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" + entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/common" "github.com/elastic/cloud-on-k8s/pkg/controller/common/annotation" @@ -32,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -41,11 +43,9 @@ var ( // Throughout those tests we'll use Kibana association for testing purposes, // but tests are the same for any resource type. kbAssociationInfo = AssociationInfo{ - AssociatedObjTemplate: func() commonv1.Associated { return &kbv1.Kibana{} }, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, - AssociatedNamer: esv1.ESNamer, + AssociatedObjTemplate: func() commonv1.Associated { return &kbv1.Kibana{} }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, + ReferencedResourceNamer: esv1.ESNamer, ExternalServiceURL: func(c k8s.Client, association commonv1.Association) (string, error) { esRef := association.AssociationRef() es := esv1.Elasticsearch{} @@ -67,12 +67,6 @@ var ( "kibanaassociation.k8s.elastic.co/namespace": associated.Namespace, } }, - UserSecretSuffix: "kibana-user", - ESUserRole: func(associated commonv1.Associated) (string, error) { - return "kibana_system", nil - }, - SetDynamicWatches: nil, - ClearDynamicWatches: nil, ReferencedResourceVersion: func(c k8s.Client, esRef types.NamespacedName) (string, error) { var es esv1.Elasticsearch if err := c.Get(context.Background(), esRef, &es); err != nil { @@ -84,6 +78,15 @@ var ( AssociationConfAnnotationNameBase: "association.k8s.elastic.co/es-conf", AssociationResourceNameLabelName: "elasticsearch.k8s.elastic.co/cluster-name", AssociationResourceNamespaceLabelName: "elasticsearch.k8s.elastic.co/cluster-namespace", + ElasticsearchUserCreation: &ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "kibana-user", + ESUserRole: func(associated commonv1.Associated) (string, error) { + return "kibana_system", nil + }, + }, } kibanaNamespace = "kbns" @@ -112,7 +115,7 @@ var ( sample := sampleKibanaWithESRef() kb := (&sample).DeepCopy() kb.Annotations = map[string]string{ - kb.AssociationConfAnnotationName(): fmt.Sprintf("{\"authSecretName\":\"kbname-kibana-user\",\"authSecretKey\":\"kbns-kbname-kibana-user\",\"caCertProvided\":true,\"caSecretName\":\"kbname-kb-es-ca\",\"url\":\"https://%s.esns.svc:9200\",\"version\":\"7.7.0\"}", svcName), + kb.EsAssociation().AssociationConfAnnotationName(): fmt.Sprintf("{\"authSecretName\":\"kbname-kibana-user\",\"authSecretKey\":\"kbns-kbname-kibana-user\",\"caCertProvided\":true,\"caSecretName\":\"kbname-kb-es-ca\",\"url\":\"https://%s.esns.svc:9200\",\"version\":\"7.7.0\"}", svcName), } return *kb } @@ -225,11 +228,6 @@ var ( }, } } - setDynamicWatches = func(t *testing.T, r Reconciler, kb kbv1.Kibana) { - t.Helper() - err := r.reconcileWatches(k8s.ExtractNamespacedName(&kb), kb.GetAssociations()) - require.NoError(t, err) - } ) type denyAllAccessReviewer struct{} @@ -344,12 +342,12 @@ func TestReconciler_Reconcile_NoESRef_Cleanup(t *testing.T) { // but with a config annotation and secrets resources to clean kb := sampleKibanaNoEsRef() kb.Annotations = sampleAssociatedKibana().Annotations - require.NotEmpty(t, kb.Annotations[kb.AssociationConfAnnotationName()]) + require.NotEmpty(t, kb.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) r := testReconciler(&kb, &kibanaUserInESNamespace, &kibanaUserInKibanaNamespace, &esCertsInKibanaNamespace) // simulate watches being set - setDynamicWatches(t, r, sampleAssociatedKibana()) + require.NoError(t, r.reconcileWatches(k8s.ExtractNamespacedName(&kb), []commonv1.Association{kb.EsAssociation()})) require.NotEmpty(t, r.watches.Secrets.Registrations()) - require.NotEmpty(t, r.watches.ElasticsearchClusters.Registrations()) + require.NotEmpty(t, r.watches.ReferencedResources.Registrations()) _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: k8s.ExtractNamespacedName(&kb)}) require.NoError(t, err) @@ -370,15 +368,16 @@ func TestReconciler_Reconcile_NoESRef_Cleanup(t *testing.T) { var updatedKibana kbv1.Kibana err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kb), &updatedKibana) require.NoError(t, err) - require.Empty(t, updatedKibana.Annotations[kb.AssociationConfAnnotationName()]) + require.Empty(t, updatedKibana.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) // should remove dynamic watches require.Empty(t, r.watches.Secrets.Registrations()) - require.Empty(t, r.watches.ElasticsearchClusters.Registrations()) + require.Empty(t, r.watches.ReferencedResources.Registrations()) + require.Empty(t, r.watches.Services.Registrations()) } func TestReconciler_Reconcile_NoES(t *testing.T) { kb := sampleAssociatedKibana() - require.NotEmpty(t, kb.Annotations[kb.AssociationConfAnnotationName()]) + require.NotEmpty(t, kb.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) // es resource does not exist r := testReconciler(&kb) _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: k8s.ExtractNamespacedName(&kb)}) @@ -389,13 +388,13 @@ func TestReconciler_Reconcile_NoES(t *testing.T) { require.NoError(t, err) require.Equal(t, commonv1.AssociationPending, updatedKibana.Status.AssociationStatus) // association conf should have been removed - require.Empty(t, updatedKibana.Annotations[kb.AssociationConfAnnotationName()]) + require.Empty(t, updatedKibana.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) } func TestReconciler_Reconcile_RBACNotAllowed(t *testing.T) { kb := sampleAssociatedKibana() - require.NotEmpty(t, kb.Annotations[kb.AssociationConfAnnotationName()]) - r := testReconciler(&kb, &sampleES, &kibanaUserInESNamespace) + require.NotEmpty(t, kb.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) + r := testReconciler(&kb, &sampleES, &kibanaUserInESNamespace, esHTTPService()) // simulate rbac association disallowed r.accessReviewer = denyAllAccessReviewer{} _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: k8s.ExtractNamespacedName(&kb)}) @@ -406,7 +405,7 @@ func TestReconciler_Reconcile_RBACNotAllowed(t *testing.T) { require.NoError(t, err) require.Equal(t, commonv1.AssociationPending, updatedKibana.Status.AssociationStatus) // association conf should be removed - require.Empty(t, updatedKibana.Annotations[kb.AssociationConfAnnotationName()]) + require.Empty(t, updatedKibana.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) // user in es namespace should be deleted var secret corev1.Secret err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kibanaUserInESNamespace), &secret) @@ -417,11 +416,11 @@ func TestReconciler_Reconcile_RBACNotAllowed(t *testing.T) { func TestReconciler_Reconcile_NewAssociation(t *testing.T) { // Kibana references ES, but no secret nor association conf exist yet kb := sampleKibanaWithESRef() - require.Empty(t, kb.Annotations[kb.AssociationConfAnnotationName()]) + require.Empty(t, kb.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) r := testReconciler(&kb, &sampleES, &esHTTPPublicCertsSecret, esHTTPService()) // no resources are watched yet require.Empty(t, r.watches.Secrets.Registrations()) - require.Empty(t, r.watches.ElasticsearchClusters.Registrations()) + require.Empty(t, r.watches.ReferencedResources.Registrations()) // run the reconciliation results, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: k8s.ExtractNamespacedName(&kb)}) require.NoError(t, err) @@ -456,28 +455,189 @@ func TestReconciler_Reconcile_NewAssociation(t *testing.T) { // should have dynamic watches set require.NotEmpty(t, r.watches.Secrets.Registrations()) - require.NotEmpty(t, r.watches.ElasticsearchClusters.Registrations()) + require.NotEmpty(t, r.watches.ReferencedResources.Registrations()) var updatedKibana kbv1.Kibana err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kb), &updatedKibana) // association conf should be set - require.Equal(t, sampleAssociatedKibana().Annotations[kb.AssociationConfAnnotationName()], updatedKibana.Annotations[kb.AssociationConfAnnotationName()]) + require.Equal(t, sampleAssociatedKibana().Annotations[kb.EsAssociation().AssociationConfAnnotationName()], updatedKibana.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) // association status should be established require.NoError(t, err) require.Equal(t, commonv1.AssociationEstablished, updatedKibana.Status.AssociationStatus) } +func TestReconciler_Reconcile_noESAuth(t *testing.T) { + // Kibana references Enterprise Search, the association controller is configured to not + // create an Elasticsearch user + ent := entv1.EnterpriseSearch{ObjectMeta: metav1.ObjectMeta{Namespace: "entns", Name: "entname"}} + entHTTPPublicCertsSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "entns", + Name: "entname-ent-http-certs-public", + }, + Data: map[string][]byte{ + "ca.crt": []byte("ca cert content"), + "tls.crt": []byte("tls cert content"), + }, + } + entHTTPService := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "entns", + Name: "entname-ent-http", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "https", + Port: 3002, + }, + }, + }, + } + kb := sampleKibanaNoEsRef() + kb.Spec.EnterpriseSearchRef = commonv1.ObjectSelector{Name: "entname", Namespace: "entns"} + + // ent public certs we expect to be copied over into the Kibana namespace + entCertsInKibanaNamespace := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: kibanaNamespace, + Name: "kbname-kb-ent-ca", + Labels: map[string]string{ + "enterprisesearch.k8s.elastic.co/name": "entname", + "enterprisesearch.k8s.elastic.co/namespace": "entns", + "kibanaassociation.k8s.elastic.co/name": "kbname", + "kibanaassociation.k8s.elastic.co/namespace": "kbns", + "kibanaassociation.k8s.elastic.co/type": "ent", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "kibana.k8s.elastic.co/v1", + Kind: "Kibana", + Name: "kbname", + Controller: &varTrue, + BlockOwnerDeletion: &varTrue, + }, + }, + }, + Data: map[string][]byte{ + "ca.crt": []byte("ca cert content"), + "tls.crt": []byte("tls cert content"), + }, + } + + kbEntAssocInfo := AssociationInfo{ + AssociatedObjTemplate: func() commonv1.Associated { return &kbv1.Kibana{} }, + ReferencedObjTemplate: func() client.Object { return &entv1.EnterpriseSearch{} }, + ExternalServiceURL: func(c k8s.Client, assoc commonv1.Association) (string, error) { + entRef := assoc.AssociationRef() + if !entRef.IsDefined() { + return "", nil + } + ent := entv1.EnterpriseSearch{} + if err := c.Get(context.Background(), entRef.NamespacedName(), &ent); err != nil { + return "", err + } + serviceName := entRef.ServiceName + if serviceName == "" { + serviceName = "entname-ent-http" + } + nsn := types.NamespacedName{Namespace: ent.Namespace, Name: serviceName} + return ServiceURL(c, nsn, ent.Spec.HTTP.Protocol()) + }, + ReferencedResourceVersion: func(c k8s.Client, entRef types.NamespacedName) (string, error) { + var ent entv1.EnterpriseSearch + err := c.Get(context.Background(), entRef, &ent) + if err != nil { + return "", err + } + return ent.Status.Version, nil + }, + ReferencedResourceNamer: entv1.Namer, + AssociationName: "kb-ent", + AssociatedShortName: "kb", + AssociationType: commonv1.EntAssociationType, + Labels: func(associated types.NamespacedName) map[string]string { + return map[string]string{ + "kibanaassociation.k8s.elastic.co/name": associated.Name, + "kibanaassociation.k8s.elastic.co/namespace": associated.Namespace, + "kibanaassociation.k8s.elastic.co/type": commonv1.EntAssociationType, + } + }, + AssociationConfAnnotationNameBase: commonv1.EntConfigAnnotationNameBase, + AssociationResourceNameLabelName: "enterprisesearch.k8s.elastic.co/name", + AssociationResourceNamespaceLabelName: "enterprisesearch.k8s.elastic.co/namespace", + ElasticsearchUserCreation: nil, // no dedicated ES user required for Kibana->Ent connection + } + + r := Reconciler{ + AssociationInfo: kbEntAssocInfo, + Client: k8s.NewFakeClient( + &kb, + &ent, + &entHTTPPublicCertsSecret, + &entHTTPService, + ), + accessReviewer: rbac.NewPermissiveAccessReviewer(), + watches: watches.NewDynamicWatches(), + recorder: record.NewFakeRecorder(10), + Parameters: operator.Parameters{ + OperatorInfo: about.OperatorInfo{ + BuildInfo: about.BuildInfo{ + Version: "1.4.0-unittest", + }, + }, + }, + logger: log.WithName("test"), + } + + // no resources are watched yet + require.Empty(t, r.watches.Secrets.Registrations()) + require.Empty(t, r.watches.ReferencedResources.Registrations()) + // run the reconciliation + results, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: k8s.ExtractNamespacedName(&kb)}) + require.NoError(t, err) + // no requeue to trigger + require.Equal(t, reconcile.Result{}, results) + + // should create the ent certs in kibana namespace + var actualEntCertsInKibanaNamespace corev1.Secret + err = r.Get(context.Background(), k8s.ExtractNamespacedName(&entCertsInKibanaNamespace), &actualEntCertsInKibanaNamespace) + require.NoError(t, err) + comparison.RequireEqual(t, &entCertsInKibanaNamespace, &actualEntCertsInKibanaNamespace) + + // should have dynamic watches set + require.NotEmpty(t, r.watches.Secrets.Registrations()) + require.NotEmpty(t, r.watches.ReferencedResources.Registrations()) + + var updatedKibana kbv1.Kibana + err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kb), &updatedKibana) + require.NoError(t, err) + // association conf should be set + require.Equal(t, "{\"authSecretName\":\"-\",\"authSecretKey\":\"\",\"caCertProvided\":true,\"caSecretName\":\"kbname-kb-ent-ca\",\"url\":\"https://entname-ent-http.entns.svc:3002\",\"version\":\"\"}", + updatedKibana.Annotations[kb.EntAssociation().AssociationConfAnnotationName()]) + // ent association status should be established + require.Equal(t, commonv1.AssociationEstablished, updatedKibana.Status.EnterpriseSearchAssociationStatus) + // but not es association status + require.Empty(t, updatedKibana.Status.AssociationStatus) + + // should not have any other secret created (no es user to to create) + secrets := corev1.SecretList{} + err = r.List(context.Background(), &secrets) + require.NoError(t, err) + require.Len(t, secrets.Items, 2) // ent cert in ent namespace + ent cert in kb namespace +} + func TestReconciler_Reconcile_CustomServiceRef(t *testing.T) { // Kibana references ES with a custom service, but neither the service nor secret nor association conf exist yet kb := sampleKibanaWithESRef() serviceName := "coordinating-only" kb.Spec.ElasticsearchRef.ServiceName = serviceName - require.Empty(t, kb.Annotations[kb.AssociationConfAnnotationName()]) + require.Empty(t, kb.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) r := testReconciler(&kb, &sampleES, &esHTTPPublicCertsSecret) // no resources are watched yet require.Empty(t, r.watches.Secrets.Registrations()) - require.Empty(t, r.watches.ElasticsearchClusters.Registrations()) + require.Empty(t, r.watches.ReferencedResources.Registrations()) // run the reconciliation results, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: k8s.ExtractNamespacedName(&kb)}) // expect and error due to the missing service @@ -485,17 +645,7 @@ func TestReconciler_Reconcile_CustomServiceRef(t *testing.T) { // also expect a re-queue to be scheduled require.Equal(t, defaultRequeue, results) - // should create the kibana user in es namespace - var actualKbUserInESNamespace corev1.Secret - err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kibanaUserInESNamespace), &actualKbUserInESNamespace) - require.NoError(t, err) - // password hash should be generated so let's ignore its exact content in the comparison - require.NotEmpty(t, actualKbUserInESNamespace.Data[user.PasswordHashField]) - expected := kibanaUserInESNamespace.DeepCopy() - expected.Data[user.PasswordHashField] = actualKbUserInESNamespace.Data[user.PasswordHashField] - comparison.RequireEqual(t, expected, &actualKbUserInESNamespace) - - // create the service + // create the missing service svc := esHTTPService() svc.Name = serviceName require.NoError(t, r.Create(context.Background(), svc)) @@ -506,6 +656,16 @@ func TestReconciler_Reconcile_CustomServiceRef(t *testing.T) { // no requeue to trigger require.Equal(t, reconcile.Result{}, results) + // should create the kibana user in es namespace + var actualKbUserInESNamespace corev1.Secret + err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kibanaUserInESNamespace), &actualKbUserInESNamespace) + require.NoError(t, err) + // password hash should be generated so let's ignore its exact content in the comparison + require.NotEmpty(t, actualKbUserInESNamespace.Data[user.PasswordHashField]) + expected := kibanaUserInESNamespace.DeepCopy() + expected.Data[user.PasswordHashField] = actualKbUserInESNamespace.Data[user.PasswordHashField] + comparison.RequireEqual(t, expected, &actualKbUserInESNamespace) + // should create the kibana user in kibana namespace var actualKbUserInKbNamespace corev1.Secret err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kibanaUserInKibanaNamespace), &actualKbUserInKbNamespace) @@ -524,18 +684,19 @@ func TestReconciler_Reconcile_CustomServiceRef(t *testing.T) { // should have dynamic watches set require.NotEmpty(t, r.watches.Secrets.Registrations()) - require.NotEmpty(t, r.watches.ElasticsearchClusters.Registrations()) + require.NotEmpty(t, r.watches.ReferencedResources.Registrations()) // including a watch for the custom service require.NotEmpty(t, t, r.watches.Services.Registrations()) var updatedKibana kbv1.Kibana err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kb), &updatedKibana) // association conf should be set - require.Equal(t, sampleAssociatedKibana(serviceName).Annotations[kb.AssociationConfAnnotationName()], updatedKibana.Annotations[kb.AssociationConfAnnotationName()]) + require.Equal(t, sampleAssociatedKibana(serviceName).Annotations[kb.EsAssociation().AssociationConfAnnotationName()], updatedKibana.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) // association status should be established require.NoError(t, err) require.Equal(t, commonv1.AssociationEstablished, updatedKibana.Status.AssociationStatus) } + func TestReconciler_Reconcile_ExistingAssociation_NoOp(t *testing.T) { // association already established, reconciliation should be a no-op kb := sampleAssociatedKibana() @@ -567,7 +728,7 @@ func TestReconciler_Reconcile_ExistingAssociation_NoOp(t *testing.T) { var updatedKibana kbv1.Kibana err = r.Get(context.Background(), k8s.ExtractNamespacedName(&kb), &updatedKibana) // association conf should be set - require.Equal(t, sampleAssociatedKibana().Annotations[kb.AssociationConfAnnotationName()], updatedKibana.Annotations[kb.AssociationConfAnnotationName()]) + require.Equal(t, sampleAssociatedKibana().Annotations[kb.EsAssociation().AssociationConfAnnotationName()], updatedKibana.Annotations[kb.EsAssociation().AssociationConfAnnotationName()]) // association status should be established require.NoError(t, err) require.Equal(t, commonv1.AssociationEstablished, updatedKibana.Status.AssociationStatus) @@ -599,7 +760,7 @@ func TestReconciler_getElasticsearch(t *testing.T) { { name: "retrieve existing Elasticsearch", runtimeObjects: []runtime.Object{&es, &associatedKibana}, - associated: &associatedKibana, + associated: associatedKibana.EsAssociation(), esRef: commonv1.ObjectSelector{Namespace: "ns", Name: "es"}, wantES: es, wantStatus: "", @@ -608,7 +769,7 @@ func TestReconciler_getElasticsearch(t *testing.T) { { name: "Elasticsearch not found: remove association conf in Kibana", runtimeObjects: []runtime.Object{&associatedKibana}, // no ES - associated: &associatedKibana, + associated: associatedKibana.EsAssociation(), esRef: commonv1.ObjectSelector{Namespace: "ns", Name: "es"}, wantES: esv1.Elasticsearch{}, wantStatus: commonv1.AssociationPending, @@ -654,9 +815,7 @@ func TestReconciler_Reconcile_MultiRef(t *testing.T) { agentAssociationInfo := AssociationInfo{ AssociationType: commonv1.ElasticsearchAssociationType, AssociatedObjTemplate: func() commonv1.Associated { return &agentv1alpha1.Agent{} }, - ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { - return true, association.AssociationRef(), nil - }, + ReferencedObjTemplate: func() client.Object { return &esv1.Elasticsearch{} }, ReferencedResourceVersion: func(c k8s.Client, esRef types.NamespacedName) (string, error) { var es esv1.Elasticsearch if err := c.Get(context.Background(), esRef, &es); err != nil { @@ -675,9 +834,9 @@ func TestReconciler_Reconcile_MultiRef(t *testing.T) { } return services.ExternalServiceURL(es), nil }, - AssociatedNamer: esv1.ESNamer, - AssociationName: "agent-es", - AssociatedShortName: "agent", + ReferencedResourceNamer: esv1.ESNamer, + AssociationName: "agent-es", + AssociatedShortName: "agent", Labels: func(associated types.NamespacedName) map[string]string { return map[string]string{ "agentassociation.k8s.elastic.co/name": associated.Name, @@ -685,13 +844,18 @@ func TestReconciler_Reconcile_MultiRef(t *testing.T) { "agentassociation.k8s.elastic.co/type": commonv1.ElasticsearchAssociationType, } }, - AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, - UserSecretSuffix: "agent-user", - ESUserRole: func(associated commonv1.Associated) (string, error) { - return "superuser", nil - }, + AssociationConfAnnotationNameBase: commonv1.ElasticsearchConfigAnnotationNameBase, AssociationResourceNameLabelName: eslabel.ClusterNameLabelName, AssociationResourceNamespaceLabelName: eslabel.ClusterNamespaceLabelName, + ElasticsearchUserCreation: &ElasticsearchUserCreation{ + ElasticsearchRef: func(c k8s.Client, association commonv1.Association) (bool, commonv1.ObjectSelector, error) { + return true, association.AssociationRef(), nil + }, + UserSecretSuffix: "agent-user", + ESUserRole: func(associated commonv1.Associated) (string, error) { + return "superuser", nil + }, + }, } // Agent with two refs @@ -927,11 +1091,11 @@ func checkWatches(t *testing.T, watches watches.DynamicWatches, expected bool) { t.Helper() if expected { require.Contains(t, watches.Secrets.Registrations(), "agentNs-agent1-es-user-watch") - require.Contains(t, watches.Secrets.Registrations(), "agentNs-agent1-ca-watch") - require.Contains(t, watches.ElasticsearchClusters.Registrations(), "agentNs-agent1-es-watch") + require.Contains(t, watches.Secrets.Registrations(), "agentNs-agent1-referenced-resource-ca-secret-watch") + require.Contains(t, watches.ReferencedResources.Registrations(), "agentNs-agent1-referenced-resource-watch") } else { require.Empty(t, watches.Secrets.Registrations()) - require.Empty(t, watches.ElasticsearchClusters.Registrations()) + require.Empty(t, watches.ReferencedResources.Registrations()) } } diff --git a/pkg/controller/association/resources_test.go b/pkg/controller/association/resources_test.go index b072474e19..6a513e6738 100644 --- a/pkg/controller/association/resources_test.go +++ b/pkg/controller/association/resources_test.go @@ -72,7 +72,7 @@ func Test_deleteOrphanedResources(t *testing.T) { // ca secret should be in Kibana namespace assert.NoError(t, c.Get(context.Background(), types.NamespacedName{ Namespace: kibanaFixture.Namespace, - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), }, &corev1.Secret{})) } @@ -111,7 +111,7 @@ func Test_deleteOrphanedResources(t *testing.T) { }, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), Namespace: kibanaFixture.Namespace, Labels: associationLabels, }, @@ -150,7 +150,7 @@ func Test_deleteOrphanedResources(t *testing.T) { }, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), Namespace: kibanaFixture.Namespace, Labels: associationLabels, }, @@ -191,7 +191,7 @@ func Test_deleteOrphanedResources(t *testing.T) { }, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), Namespace: kibanaFixture.Namespace, }, }, @@ -230,7 +230,7 @@ func Test_deleteOrphanedResources(t *testing.T) { }, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), Namespace: kibanaFixture.Namespace, Labels: associationLabels, }, @@ -248,7 +248,7 @@ func Test_deleteOrphanedResources(t *testing.T) { }, &corev1.Secret{})) assert.Error(t, c.Get(context.Background(), types.NamespacedName{ Namespace: kibanaFixture.Spec.ElasticsearchRef.Namespace, - Name: CACertSecretName(&kibanaFixture, kibanaESAssociationName), + Name: CACertSecretName(kibanaFixture.EsAssociation(), kibanaESAssociationName), }, &corev1.Secret{})) }, wantErr: false, @@ -310,7 +310,7 @@ func Test_deleteOrphanedResources(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := k8s.NewFakeClient(tt.initialObjects...) - if err := deleteOrphanedResources(context.Background(), c, info, tt.kibana.AssociationRef().WithDefaultNamespace(tt.kibana.Namespace).NamespacedName(), tt.kibana.GetAssociations()); (err != nil) != tt.wantErr { + if err := deleteOrphanedResources(context.Background(), c, info, tt.kibana.EsAssociation().AssociationRef().WithDefaultNamespace(tt.kibana.Namespace).NamespacedName(), tt.kibana.GetAssociations()); (err != nil) != tt.wantErr { t.Errorf("deleteOrphanedResources() error = %v, wantErr %v", err, tt.wantErr) } if tt.postCondition != nil { diff --git a/pkg/controller/association/service.go b/pkg/controller/association/service.go index 2ac1d0de02..f11aab1536 100644 --- a/pkg/controller/association/service.go +++ b/pkg/controller/association/service.go @@ -18,7 +18,7 @@ import ( func ServiceURL(c k8s.Client, serviceNSN types.NamespacedName, protocol string) (string, error) { var svc corev1.Service if err := c.Get(context.Background(), serviceNSN, &svc); err != nil { - return "", fmt.Errorf("while fetching refernced service: %w", err) + return "", fmt.Errorf("while fetching referenced service: %w", err) } port, err := findPortFor(protocol, svc) if err != nil { diff --git a/pkg/controller/association/user.go b/pkg/controller/association/user.go index 9360c667bd..99ff10ed7e 100644 --- a/pkg/controller/association/user.go +++ b/pkg/controller/association/user.go @@ -72,8 +72,9 @@ func UserSecretKeySelector(association commonv1.Association, userSuffix string) } } -// ReconcileEsUser creates a User resource and a corresponding secret or updates those as appropriate. -func ReconcileEsUser( +// reconcileEsUserSecret creates or updates the Elasticsearch user secrets in the Elasticsearch namespace +// and the associated resource namespace. +func reconcileEsUserSecret( ctx context.Context, c k8s.Client, association commonv1.Association, diff --git a/pkg/controller/association/user_test.go b/pkg/controller/association/user_test.go index 1e025eabbb..2bf769c334 100644 --- a/pkg/controller/association/user_test.go +++ b/pkg/controller/association/user_test.go @@ -248,10 +248,10 @@ func Test_reconcileEsUser(t *testing.T) { for _, tt := range tests { c := k8s.NewFakeClient(tt.args.initialObjects...) t.Run(tt.name, func(t *testing.T) { - if err := ReconcileEsUser( + if err := reconcileEsUserSecret( context.Background(), c, - &tt.args.kibana, + tt.args.kibana.EsAssociation(), map[string]string{ associationLabelName: tt.args.kibana.Name, associationLabelNamespace: tt.args.kibana.Namespace, diff --git a/pkg/controller/common/association/association.go b/pkg/controller/common/association/association.go index 042a5bcb73..bff6597e2d 100644 --- a/pkg/controller/common/association/association.go +++ b/pkg/controller/common/association/association.go @@ -34,6 +34,9 @@ func writeAuthSecretToConfigHash(client k8s.Client, assoc commonv1.Association, if !assocConf.AuthIsConfigured() { return nil } + if assocConf.NoAuthRequired() { + return nil + } authSecretNsName := types.NamespacedName{ Name: assocConf.GetAuthSecretName(), diff --git a/pkg/controller/common/watches/state.go b/pkg/controller/common/watches/state.go index 65cbc4f059..4802ba2b54 100644 --- a/pkg/controller/common/watches/state.go +++ b/pkg/controller/common/watches/state.go @@ -7,20 +7,18 @@ package watches // NewDynamicWatches creates an initialized DynamicWatches container. func NewDynamicWatches() DynamicWatches { return DynamicWatches{ - Secrets: NewDynamicEnqueueRequest(), - Services: NewDynamicEnqueueRequest(), - Pods: NewDynamicEnqueueRequest(), - ElasticsearchClusters: NewDynamicEnqueueRequest(), - Kibanas: NewDynamicEnqueueRequest(), + Secrets: NewDynamicEnqueueRequest(), + Services: NewDynamicEnqueueRequest(), + Pods: NewDynamicEnqueueRequest(), + ReferencedResources: NewDynamicEnqueueRequest(), } } // DynamicWatches contains stateful dynamic watches. Intended as facility to pass around stateful dynamic watches and // give each of them an identity. type DynamicWatches struct { - Secrets *DynamicEnqueueRequest - Services *DynamicEnqueueRequest - Pods *DynamicEnqueueRequest - ElasticsearchClusters *DynamicEnqueueRequest - Kibanas *DynamicEnqueueRequest + Secrets *DynamicEnqueueRequest + Services *DynamicEnqueueRequest + Pods *DynamicEnqueueRequest + ReferencedResources *DynamicEnqueueRequest } diff --git a/pkg/controller/enterprisesearch/config.go b/pkg/controller/enterprisesearch/config.go index 081e4c9600..c769119761 100644 --- a/pkg/controller/enterprisesearch/config.go +++ b/pkg/controller/enterprisesearch/config.go @@ -20,7 +20,6 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/controller/common/settings" "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/pkg/controller/common/volume" - "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" netutil "github.com/elastic/cloud-on-k8s/pkg/utils/net" corev1 "k8s.io/api/core/v1" @@ -42,12 +41,12 @@ const ( ) func ConfigSecretVolume(ent entv1.EnterpriseSearch) volume.SecretVolume { - return volume.NewSecretVolume(name.Config(ent.Name), "config", ConfigMountPath, ConfigFilename, 0444) + return volume.NewSecretVolume(ConfigName(ent.Name), "config", ConfigMountPath, ConfigFilename, 0444) } func ReadinessProbeSecretVolume(ent entv1.EnterpriseSearch) volume.SecretVolume { // reuse the config secret - return volume.NewSecretVolume(name.Config(ent.Name), "readiness-probe", ReadinessProbeMountPath, ReadinessProbeFilename, 0444) + return volume.NewSecretVolume(ConfigName(ent.Name), "readiness-probe", ReadinessProbeMountPath, ReadinessProbeFilename, 0444) } // Reconcile reconciles the configuration of Enterprise Search: it generates the right configuration and @@ -75,7 +74,7 @@ func ReconcileConfig(driver driver.Interface, ent entv1.EnterpriseSearch, ipFami expectedConfigSecret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: ent.Namespace, - Name: name.Config(ent.Name), + Name: ConfigName(ent.Name), Labels: common.AddCredentialsLabel(Labels(ent.Name)), }, Data: map[string][]byte{ @@ -222,7 +221,7 @@ func getExistingConfig(client k8s.Client, ent entv1.EnterpriseSearch) (*settings var secret corev1.Secret key := types.NamespacedName{ Namespace: ent.Namespace, - Name: name.Config(ent.Name), + Name: ConfigName(ent.Name), } err := client.Get(context.Background(), key, &secret) if err != nil && apierrors.IsNotFound(err) { @@ -311,7 +310,7 @@ func tlsConfig(ent entv1.EnterpriseSearch) *settings.CanonicalConfig { if !ent.Spec.HTTP.TLS.Enabled() { return settings.NewCanonicalConfig() } - certsDir := certificates.HTTPCertSecretVolume(name.EntNamer, ent.Name).VolumeMount().MountPath + certsDir := certificates.HTTPCertSecretVolume(entv1.Namer, ent.Name).VolumeMount().MountPath return settings.MustCanonicalConfig(map[string]interface{}{ "ent_search.ssl.enabled": true, "ent_search.ssl.certificate": filepath.Join(certsDir, certificates.CertFileName), diff --git a/pkg/controller/enterprisesearch/deployment.go b/pkg/controller/enterprisesearch/deployment.go index 3c0502483a..216206f587 100644 --- a/pkg/controller/enterprisesearch/deployment.go +++ b/pkg/controller/enterprisesearch/deployment.go @@ -13,7 +13,6 @@ import ( entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/common/deployment" "github.com/elastic/cloud-on-k8s/pkg/controller/common/tracing" - entName "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/maps" ) @@ -39,7 +38,7 @@ func (r *ReconcileEnterpriseSearch) deploymentParams(ent entv1.EnterpriseSearch, podSpec.Labels = maps.MergePreservingExistingKeys(podSpec.Labels, podLabels) return deployment.Params{ - Name: entName.Deployment(ent.Name), + Name: DeploymentName(ent.Name), Namespace: ent.Namespace, Replicas: ent.Spec.Count, Selector: deploymentLabels, diff --git a/pkg/controller/enterprisesearch/enterprisesearch_controller.go b/pkg/controller/enterprisesearch/enterprisesearch_controller.go index 4a6b4e978b..41ba387354 100644 --- a/pkg/controller/enterprisesearch/enterprisesearch_controller.go +++ b/pkg/controller/enterprisesearch/enterprisesearch_controller.go @@ -24,7 +24,6 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/controller/common/tracing" "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/pkg/controller/common/watches" - entName "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" ulog "github.com/elastic/cloud-on-k8s/pkg/utils/log" "go.elastic.co/apm" @@ -182,7 +181,7 @@ func (r *ReconcileEnterpriseSearch) onDelete(obj types.NamespacedName) error { // Clean up watches r.dynamicWatches.Secrets.RemoveHandlerForKey(common.ConfigRefWatchName(obj)) // Clean up watches set on custom http tls certificates - r.dynamicWatches.Secrets.RemoveHandlerForKey(certificates.CertificateWatchKey(entName.EntNamer, obj.Name)) + r.dynamicWatches.Secrets.RemoveHandlerForKey(certificates.CertificateWatchKey(entv1.Namer, obj.Name)) return reconciler.GarbageCollectSoftOwnedSecrets(r.Client, obj, entv1.Kind) } @@ -211,7 +210,7 @@ func (r *ReconcileEnterpriseSearch) doReconcile(ctx context.Context, ent entv1.E DynamicWatches: r.DynamicWatches(), Owner: &ent, TLSOptions: ent.Spec.HTTP.TLS, - Namer: entName.EntNamer, + Namer: entv1.Namer, Labels: Labels(ent.Name), Services: []corev1.Service{*svc}, CACertRotation: r.CACertRotation, @@ -310,7 +309,7 @@ func NewService(ent entv1.EnterpriseSearch) *corev1.Service { } svc.ObjectMeta.Namespace = ent.Namespace - svc.ObjectMeta.Name = entName.HTTPService(ent.Name) + svc.ObjectMeta.Name = HTTPServiceName(ent.Name) labels := Labels(ent.Name) ports := []corev1.ServicePort{ @@ -336,7 +335,7 @@ func buildConfigHash(c k8s.Client, ent entv1.EnterpriseSearch, configSecret core // - in the Enterprise Search TLS certificates if ent.Spec.HTTP.TLS.Enabled() { var tlsCertSecret corev1.Secret - tlsSecretKey := types.NamespacedName{Namespace: ent.Namespace, Name: certificates.InternalCertsSecretName(entName.EntNamer, ent.Name)} + tlsSecretKey := types.NamespacedName{Namespace: ent.Namespace, Name: certificates.InternalCertsSecretName(entv1.Namer, ent.Name)} if err := c.Get(context.Background(), tlsSecretKey, &tlsCertSecret); err != nil { return "", err } diff --git a/pkg/controller/enterprisesearch/enterprisesearch_controller_test.go b/pkg/controller/enterprisesearch/enterprisesearch_controller_test.go index e42f7efb07..2bf0ae4f06 100644 --- a/pkg/controller/enterprisesearch/enterprisesearch_controller_test.go +++ b/pkg/controller/enterprisesearch/enterprisesearch_controller_test.go @@ -26,7 +26,6 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/controller/common/certificates" "github.com/elastic/cloud-on-k8s/pkg/controller/common/operator" "github.com/elastic/cloud-on-k8s/pkg/controller/common/watches" - entName "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" ) @@ -151,7 +150,7 @@ func TestReconcileEnterpriseSearch_Reconcile_Create_Update_Resources(t *testing. checkResources := func() { // should create a service var service corev1.Service - err := r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: entName.HTTPService(sample.Name)}, &service) + err := r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: HTTPServiceName(sample.Name)}, &service) require.NoError(t, err) require.Equal(t, int32(3002), service.Spec.Ports[0].Port) @@ -211,7 +210,7 @@ func TestReconcileEnterpriseSearch_Reconcile_Create_Update_Resources(t *testing. require.NoError(t, err) // delete the http service var service corev1.Service - err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: entName.HTTPService(sample.Name)}, &service) + err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: HTTPServiceName(sample.Name)}, &service) require.NoError(t, err) err = r.Client.Delete(context.Background(), &service) require.NoError(t, err) @@ -266,7 +265,7 @@ func TestReconcileEnterpriseSearch_doReconcile_AssociationDelaysVersionUpgrade(t require.NoError(t, err) // the Enterprise Search deployment should be created and specify version 7.7.0 var dep appsv1.Deployment - err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: entName.Deployment(ent.Name)}, &dep) + err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: DeploymentName(ent.Name)}, &dep) require.NoError(t, err) require.Equal(t, "7.7.0", dep.Spec.Template.Labels[VersionLabelName]) // retrieve the updated ent resource @@ -279,7 +278,7 @@ func TestReconcileEnterpriseSearch_doReconcile_AssociationDelaysVersionUpgrade(t require.NoError(t, err) _, err = r.doReconcile(context.Background(), ent) require.NoError(t, err) - err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: entName.Deployment(ent.Name)}, &dep) + err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: DeploymentName(ent.Name)}, &dep) require.NoError(t, err) require.Equal(t, "7.7.0", dep.Spec.Template.Labels[VersionLabelName]) // retrieve the updated ent resource @@ -291,7 +290,7 @@ func TestReconcileEnterpriseSearch_doReconcile_AssociationDelaysVersionUpgrade(t ent.SetAssociationConf(assocConf) _, err = r.doReconcile(context.Background(), ent) require.NoError(t, err) - err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: entName.Deployment(ent.Name)}, &dep) + err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: DeploymentName(ent.Name)}, &dep) require.NoError(t, err) require.Equal(t, "7.8.0", dep.Spec.Template.Labels[VersionLabelName]) // retrieve the updated ent resource @@ -304,7 +303,7 @@ func TestReconcileEnterpriseSearch_doReconcile_AssociationDelaysVersionUpgrade(t require.NoError(t, err) _, err = r.doReconcile(context.Background(), ent) require.NoError(t, err) - err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: entName.Deployment(ent.Name)}, &dep) + err = r.Client.Get(context.Background(), types.NamespacedName{Namespace: "ns", Name: DeploymentName(ent.Name)}, &dep) require.NoError(t, err) require.Equal(t, "7.8.1", dep.Spec.Template.Labels[VersionLabelName]) } @@ -558,7 +557,7 @@ func Test_buildConfigHash(t *testing.T) { }, } tlsCertsSecret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Namespace: ent.Namespace, Name: certificates.InternalCertsSecretName(entName.EntNamer, ent.Name)}, + ObjectMeta: metav1.ObjectMeta{Namespace: ent.Namespace, Name: certificates.InternalCertsSecretName(entv1.Namer, ent.Name)}, Data: map[string][]byte{ certificates.CertFileName: []byte("cert-data"), }, diff --git a/pkg/controller/enterprisesearch/labels.go b/pkg/controller/enterprisesearch/labels.go index 0feef46a16..81fea2622b 100644 --- a/pkg/controller/enterprisesearch/labels.go +++ b/pkg/controller/enterprisesearch/labels.go @@ -14,6 +14,8 @@ const ( Type = "enterprise-search" // EnterpriseSearchNameLabelName used to represent an EnterpriseSearch in k8s resources. EnterpriseSearchNameLabelName = "enterprisesearch.k8s.elastic.co/name" + // EnterpriseSearchNamespaceLabelName used to represent an EnterpriseSearch in k8s resources. + EnterpriseSearchNamespaceLabelName = "enterprisesearch.k8s.elastic.co/namespace" // VersionLabelName is a label used to track the version of an Enterprise Search Pod. VersionLabelName = "enterprisesearch.k8s.elastic.co/version" ) diff --git a/pkg/controller/enterprisesearch/name.go b/pkg/controller/enterprisesearch/name.go new file mode 100644 index 0000000000..0cc9aaabb2 --- /dev/null +++ b/pkg/controller/enterprisesearch/name.go @@ -0,0 +1,24 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package enterprisesearch + +import entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" + +const ( + httpServiceSuffix = "http" + configSuffix = "config" +) + +func HTTPServiceName(entName string) string { + return entv1.Namer.Suffix(entName, httpServiceSuffix) +} + +func DeploymentName(entName string) string { + return entv1.Namer.Suffix(entName) +} + +func ConfigName(entName string) string { + return entv1.Namer.Suffix(entName, configSuffix) +} diff --git a/pkg/controller/enterprisesearch/name/name.go b/pkg/controller/enterprisesearch/name/name.go deleted file mode 100644 index 69ee42cd9c..0000000000 --- a/pkg/controller/enterprisesearch/name/name.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License; -// you may not use this file except in compliance with the Elastic License. - -package name - -import ( - common_name "github.com/elastic/cloud-on-k8s/pkg/controller/common/name" -) - -const ( - httpServiceSuffix = "http" - configSuffix = "config" -) - -// EntNamer is a Namer that is configured with the defaults for resources related to an EnterpriseSearch resource. -var EntNamer = common_name.NewNamer("ent") - -func HTTPService(entName string) string { - return EntNamer.Suffix(entName, httpServiceSuffix) -} - -func Deployment(entName string) string { - return EntNamer.Suffix(entName) -} - -func Config(entName string) string { - return EntNamer.Suffix(entName, configSuffix) -} diff --git a/pkg/controller/enterprisesearch/pod.go b/pkg/controller/enterprisesearch/pod.go index bf90832f02..d4a1c7b726 100644 --- a/pkg/controller/enterprisesearch/pod.go +++ b/pkg/controller/enterprisesearch/pod.go @@ -15,7 +15,6 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/controller/common/container" "github.com/elastic/cloud-on-k8s/pkg/controller/common/defaults" "github.com/elastic/cloud-on-k8s/pkg/controller/common/volume" - "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" ) const ( @@ -100,6 +99,6 @@ func withHTTPCertsVolume(builder *defaults.PodTemplateBuilder, ent entv1.Enterpr if !ent.Spec.HTTP.TLS.Enabled() { return builder } - vol := certificates.HTTPCertSecretVolume(name.EntNamer, ent.Name) + vol := certificates.HTTPCertSecretVolume(entv1.Namer, ent.Name) return builder.WithVolumes(vol.Volume()).WithVolumeMounts(vol.VolumeMount()) } diff --git a/pkg/controller/enterprisesearch/version_upgrade.go b/pkg/controller/enterprisesearch/version_upgrade.go index 9b6b13512b..af2e2504ca 100644 --- a/pkg/controller/enterprisesearch/version_upgrade.go +++ b/pkg/controller/enterprisesearch/version_upgrade.go @@ -27,7 +27,6 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/controller/common/certificates" "github.com/elastic/cloud-on-k8s/pkg/controller/common/events" "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" - entName "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/pkg/utils/net" "github.com/elastic/cloud-on-k8s/pkg/utils/stringsutil" @@ -197,7 +196,7 @@ func (r *VersionUpgrade) setReadOnlyMode(ctx context.Context, enabled bool) erro // serviceURL builds the URL of the Enterprise Search service. func (r *VersionUpgrade) serviceURL() string { return fmt.Sprintf("%s://%s.%s.svc:%d", - r.ent.Spec.HTTP.Protocol(), entName.HTTPService(r.ent.Name), r.ent.Namespace, HTTPPort) + r.ent.Spec.HTTP.Protocol(), HTTPServiceName(r.ent.Name), r.ent.Namespace, HTTPPort) } // readOnlyModeRequest builds the HTTP request to toggle the read-only mode on Enterprise Search. @@ -226,7 +225,7 @@ func (r *VersionUpgrade) readOnlyModeRequest(enabled bool) (*http.Request, error // specified in the EnterpriseSearch resource. func (r *VersionUpgrade) isVersionUpgrade(expectedVersion version.Version) (bool, error) { var deployment appsv1.Deployment - nsn := types.NamespacedName{Name: entName.Deployment(r.ent.Name), Namespace: r.ent.Namespace} + nsn := types.NamespacedName{Name: DeploymentName(r.ent.Name), Namespace: r.ent.Namespace} err := r.k8sClient.Get(context.Background(), nsn, &deployment) if err != nil { if apierrors.IsNotFound(err) { @@ -276,7 +275,7 @@ func (r *VersionUpgrade) retrieveTLSCerts() ([]*x509.Certificate, error) { var certsSecret corev1.Secret nsn := types.NamespacedName{ Namespace: r.ent.Namespace, - Name: certificates.InternalCertsSecretName(entName.EntNamer, r.ent.Name), + Name: certificates.InternalCertsSecretName(entv1.Namer, r.ent.Name), } if err := r.k8sClient.Get(context.Background(), nsn, &certsSecret); err != nil { return nil, err diff --git a/pkg/controller/enterprisesearch/version_upgrade_test.go b/pkg/controller/enterprisesearch/version_upgrade_test.go index 2347dd33d1..8f6f1f2395 100644 --- a/pkg/controller/enterprisesearch/version_upgrade_test.go +++ b/pkg/controller/enterprisesearch/version_upgrade_test.go @@ -22,7 +22,6 @@ import ( entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/common" "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" - entName "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" ) @@ -66,7 +65,7 @@ func podWithVersion(name string, version string) *corev1.Pod { func deploymentWithVersion(version string) *appsv1.Deployment { return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: entName.Deployment("ent")}, + ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: DeploymentName("ent")}, Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ VersionLabelName: version, diff --git a/pkg/controller/kibana/config_settings.go b/pkg/controller/kibana/config_settings.go index ecfb1681ea..e43b773790 100644 --- a/pkg/controller/kibana/config_settings.go +++ b/pkg/controller/kibana/config_settings.go @@ -7,6 +7,7 @@ package kibana import ( "context" "path" + "path/filepath" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" @@ -35,6 +36,8 @@ const ( // esCertsVolumeMountPath is the directory containing Elasticsearch certificates. esCertsVolumeMountPath = "/usr/share/kibana/config/elasticsearch-certs" + // entCertsVolumeMountPath is the directory into which trusted Enterprise Search HTTP CA certs are mounted. + entCertsVolumeMountPath = "/usr/share/kibana/config/ent-certs" ) // Constants to use for the Kibana configuration settings. @@ -56,6 +59,10 @@ const ( ElasticsearchHosts = "elasticsearch.hosts" + EnterpriseSearchHost = "enterpriseSearch.host" + EnterpriseSearchSslCertificateAuthorities = "enterpriseSearch.ssl.certificateAuthorities" + EnterpriseSearchSslVerificationMode = "enterpriseSearch.ssl.verificationMode" + ServerSSLEnabled = "server.ssl.enabled" ServerSSLCertificate = "server.ssl.certificate" ServerSSLKey = "server.ssl.key" @@ -83,11 +90,11 @@ func NewConfigSettings(ctx context.Context, client k8s.Client, kb kbv1.Kibana, v return CanonicalConfig{}, err } + // parse user-provided settings specConfig := kb.Spec.Config if specConfig == nil { specConfig = &commonv1.Config{} } - userSettings, err := settings.NewCanonicalConfigFrom(specConfig.Data) if err != nil { return CanonicalConfig{}, err @@ -96,20 +103,22 @@ func NewConfigSettings(ctx context.Context, client k8s.Client, kb kbv1.Kibana, v cfg := settings.MustCanonicalConfig(baseSettings(&kb, ipFamily)) kibanaTLSCfg := settings.MustCanonicalConfig(kibanaTLSSettings(kb)) versionSpecificCfg := VersionDefaults(&kb, v) + entSearchCfg := settings.MustCanonicalConfig(enterpriseSearchSettings(kb)) - if !kb.RequiresAssociation() { + if !kb.EsAssociation().AssociationConf().IsConfigured() { // merge the configuration with userSettings last so they take precedence if err := cfg.MergeWith( reusableSettings, versionSpecificCfg, kibanaTLSCfg, + entSearchCfg, userSettings); err != nil { return CanonicalConfig{}, err } return CanonicalConfig{cfg}, nil } - username, password, err := association.ElasticsearchAuthSettings(client, &kb) + username, password, err := association.ElasticsearchAuthSettings(client, kb.EsAssociation()) if err != nil { return CanonicalConfig{}, err } @@ -119,6 +128,7 @@ func NewConfigSettings(ctx context.Context, client k8s.Client, kb kbv1.Kibana, v filteredReusableSettings, versionSpecificCfg, kibanaTLSCfg, + entSearchCfg, settings.MustCanonicalConfig(elasticsearchTLSSettings(kb)), settings.MustCanonicalConfig( map[string]interface{}{ @@ -229,8 +239,9 @@ func baseSettings(kb *kbv1.Kibana, ipFamily corev1.IPFamily) map[string]interfac XpackMonitoringUIContainerElasticsearchEnabled: true, } - if kb.RequiresAssociation() { - conf[ElasticsearchHosts] = []string{kb.AssociationConf().GetURL()} + assocConf := kb.EsAssociation().AssociationConf() + if assocConf.URLIsConfigured() { + conf[ElasticsearchHosts] = []string{assocConf.GetURL()} } return conf @@ -252,7 +263,7 @@ func elasticsearchTLSSettings(kb kbv1.Kibana) map[string]interface{} { ElasticsearchSslVerificationMode: "certificate", } - if kb.AssociationConf().GetCACertProvided() { + if kb.EsAssociation().AssociationConf().GetCACertProvided() { esCertsVolumeMountPath := esCaCertSecretVolume(kb).VolumeMount().MountPath cfg[ElasticsearchSslCertificateAuthorities] = path.Join(esCertsVolumeMountPath, certificates.CAFileName) } @@ -263,8 +274,33 @@ func elasticsearchTLSSettings(kb kbv1.Kibana) map[string]interface{} { // esCaCertSecretVolume returns a SecretVolume to hold the Elasticsearch CA certs for the given Kibana resource. func esCaCertSecretVolume(kb kbv1.Kibana) volume.SecretVolume { return volume.NewSecretVolumeWithMountPath( - kb.AssociationConf().GetCASecretName(), + kb.EsAssociation().AssociationConf().GetCASecretName(), "elasticsearch-certs", esCertsVolumeMountPath, ) } + +// entCaCertSecretVolume returns a SecretVolume to hold the EnterpriseSearch CA certs for the given Kibana resource. +func entCaCertSecretVolume(kb kbv1.Kibana) volume.SecretVolume { + return volume.NewSecretVolumeWithMountPath( + kb.EntAssociation().AssociationConf().GetCASecretName(), + "ent-certs", + entCertsVolumeMountPath, + ) +} + +func enterpriseSearchSettings(kb kbv1.Kibana) map[string]interface{} { + cfg := map[string]interface{}{} + assocConf := kb.EntAssociation().AssociationConf() + if assocConf.URLIsConfigured() { + cfg[EnterpriseSearchHost] = assocConf.GetURL() + } + if assocConf.CAIsConfigured() { + cfg[EnterpriseSearchSslCertificateAuthorities] = filepath.Join(entCertsVolumeMountPath, certificates.CAFileName) + // Rely on "certificate" verification mode rather than "full" to allow Kibana + // to connect to Enterprise Search through the k8s-internal service DNS name + // even though the user-provided certificate may only specify a public-facing DNS. + cfg[EnterpriseSearchSslVerificationMode] = "certificate" + } + return cfg +} diff --git a/pkg/controller/kibana/config_settings_test.go b/pkg/controller/kibana/config_settings_test.go index 6cc2fd457e..bbcf22d0cc 100644 --- a/pkg/controller/kibana/config_settings_test.go +++ b/pkg/controller/kibana/config_settings_test.go @@ -43,7 +43,7 @@ xpack: monitoring.ui.container.elasticsearch.enabled: true `) -var associationConfig = []byte(` +var esAssociationConfig = []byte(` elasticsearch: hosts: - "https://es-url:9200" @@ -54,6 +54,14 @@ elasticsearch: verificationMode: certificate `) +var entAssociationConfig = []byte(` +enterpriseSearch: + host: https://ent-url:3002 + ssl: + certificateAuthorities: /usr/share/kibana/config/ent-certs/ca.crt + verificationMode: certificate +`) + func Test_reuseOrGenerateSecrets(t *testing.T) { defaultKb := mkKibana() type args struct { @@ -100,7 +108,7 @@ func Test_reuseOrGenerateSecrets(t *testing.T) { &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: SecretName(defaultKb)}, Data: map[string][]byte{ - SettingsFilename: associationConfig, + SettingsFilename: esAssociationConfig, }, }, ), @@ -124,7 +132,7 @@ func Test_reuseOrGenerateSecrets(t *testing.T) { &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: SecretName(defaultKb)}, Data: map[string][]byte{ - SettingsFilename: associationConfig, + SettingsFilename: esAssociationConfig, }, }, ), @@ -231,12 +239,12 @@ func TestNewConfigSettings(t *testing.T) { wantErr: false, }, { - name: "with Association", + name: "with elasticsearch Association", args: args{ kb: func() kbv1.Kibana { kb := mkKibana() kb.Spec.ElasticsearchRef = commonv1.ObjectSelector{Name: "test-es"} - kb.SetAssociationConf(&commonv1.AssociationConf{ + kb.EsAssociation().SetAssociationConf(&commonv1.AssociationConf{ AuthSecretName: "auth-secret", AuthSecretKey: "elastic", CASecretName: "ca-secret", @@ -270,7 +278,47 @@ func TestNewConfigSettings(t *testing.T) { want: func() []byte { cfg, err := settings.ParseConfig(defaultConfig) require.NoError(t, err) - assocCfg, err := settings.ParseConfig(associationConfig) + assocCfg, err := settings.ParseConfig(esAssociationConfig) + require.NoError(t, err) + require.NoError(t, cfg.MergeWith(assocCfg)) + bytes, err := cfg.Render() + require.NoError(t, err) + return bytes + }(), + wantErr: false, + }, + { + name: "with Enterprise Search Association", + args: args{ + kb: func() kbv1.Kibana { + kb := mkKibana() + kb.Spec.EnterpriseSearchRef = commonv1.ObjectSelector{Name: "test-ent"} + kb.EntAssociation().SetAssociationConf(&commonv1.AssociationConf{ + AuthSecretName: "-", + AuthSecretKey: "", + CASecretName: "ent-ca-secret", + CACertProvided: true, + URL: "https://ent-url:3002", + }) + return kb + }, + client: k8s.NewFakeClient( + existingSecret, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ent-ca-secret", + }, + Data: map[string][]byte{ + "ca.crt": []byte("certificate"), + }, + }, + ), + ipFamily: corev1.IPv4Protocol, + }, + want: func() []byte { + cfg, err := settings.ParseConfig(defaultConfig) + require.NoError(t, err) + assocCfg, err := settings.ParseConfig(entAssociationConfig) require.NoError(t, err) require.NoError(t, cfg.MergeWith(assocCfg)) bytes, err := cfg.Render() @@ -278,6 +326,76 @@ func TestNewConfigSettings(t *testing.T) { return bytes }(), wantErr: false, + }, { + name: "with Elasticsearch and Enterprise Search associations", + args: args{ + kb: func() kbv1.Kibana { + kb := mkKibana() + kb.Spec.ElasticsearchRef = commonv1.ObjectSelector{Name: "test-es"} + kb.EsAssociation().SetAssociationConf(&commonv1.AssociationConf{ + AuthSecretName: "auth-secret", + AuthSecretKey: "elastic", + CASecretName: "ca-secret", + CACertProvided: true, + URL: "https://es-url:9200", + }) + kb.Spec.EnterpriseSearchRef = commonv1.ObjectSelector{Name: "test-ent"} + kb.EntAssociation().SetAssociationConf(&commonv1.AssociationConf{ + AuthSecretName: "-", + AuthSecretKey: "", + CASecretName: "ent-ca-secret", + CACertProvided: true, + URL: "https://ent-url:3002", + }) + return kb + }, + client: k8s.NewFakeClient( + existingSecret, + // ent certs + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ent-ca-secret", + }, + Data: map[string][]byte{ + "ca.crt": []byte("certificate"), + }, + }, + // es auth + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "auth-secret", + Namespace: mkKibana().Namespace, + }, + Data: map[string][]byte{ + "elastic": []byte("password"), + }, + }, + // es certs + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-secret", + }, + Data: map[string][]byte{ + "ca.crt": []byte("certificate"), + }, + }, + ), + ipFamily: corev1.IPv4Protocol, + }, + want: func() []byte { + cfg, err := settings.ParseConfig(defaultConfig) + require.NoError(t, err) + esAssocCfg, err := settings.ParseConfig(esAssociationConfig) + require.NoError(t, err) + entAssocCfg, err := settings.ParseConfig(entAssociationConfig) + require.NoError(t, err) + require.NoError(t, cfg.MergeWith(esAssocCfg)) + require.NoError(t, cfg.MergeWith(entAssocCfg)) + bytes, err := cfg.Render() + require.NoError(t, err) + return bytes + }(), + wantErr: false, }, { name: "with user config", diff --git a/pkg/controller/kibana/driver.go b/pkg/controller/kibana/driver.go index b5465a00a0..e675580928 100644 --- a/pkg/controller/kibana/driver.go +++ b/pkg/controller/kibana/driver.go @@ -96,7 +96,10 @@ func (d *driver) Reconcile( params operator.Parameters, ) *reconciler.Results { results := reconciler.NewResult(ctx) - if !association.IsConfiguredIfSet(kb, d.recorder) { + if !association.IsConfiguredIfSet(kb.EsAssociation(), d.recorder) { + return results + } + if !association.IsConfiguredIfSet(kb.EntAssociation(), d.recorder) { return results } @@ -262,11 +265,16 @@ func (d *driver) deploymentParams(kb *kbv1.Kibana) (deployment.Params, error) { func (d *driver) buildVolumes(kb *kbv1.Kibana) []commonvolume.VolumeLike { volumes := []commonvolume.VolumeLike{DataVolume, ConfigSharedVolume, ConfigVolume(*kb)} - if kb.AssociationConf().CAIsConfigured() { + if kb.EsAssociation().AssociationConf().CAIsConfigured() { esCertsVolume := esCaCertSecretVolume(*kb) volumes = append(volumes, esCertsVolume) } + if kb.EntAssociation().AssociationConf().CAIsConfigured() { + entCertsVolume := entCaCertSecretVolume(*kb) + volumes = append(volumes, entCertsVolume) + } + if kb.Spec.HTTP.TLS.Enabled() { httpCertsVolume := certificates.HTTPCertSecretVolume(Namer, kb.Name) volumes = append(volumes, httpCertsVolume) diff --git a/pkg/controller/kibana/driver_test.go b/pkg/controller/kibana/driver_test.go index 4e7354ade2..124251380b 100644 --- a/pkg/controller/kibana/driver_test.go +++ b/pkg/controller/kibana/driver_test.go @@ -602,7 +602,7 @@ func kibanaFixture() *kbv1.Kibana { }, } - kbFixture.SetAssociationConf(&commonv1.AssociationConf{ + kbFixture.EsAssociation().SetAssociationConf(&commonv1.AssociationConf{ AuthSecretName: "test-auth", AuthSecretKey: "kibana-user", CASecretName: "es-ca-secret", diff --git a/pkg/utils/k8s/k8sutils.go b/pkg/utils/k8s/k8sutils.go index 473fe92876..84ecf12aa5 100644 --- a/pkg/utils/k8s/k8sutils.go +++ b/pkg/utils/k8s/k8sutils.go @@ -49,6 +49,19 @@ func ExtractNamespacedName(object metav1.Object) types.NamespacedName { } } +// ObjectExists returns true if the object pointed by ref exists. +// typedReceiver acts as a generic object but must be of the desired object underlying type. +func ObjectExists(c Client, ref types.NamespacedName, typedReceiver client.Object) (bool, error) { + err := c.Get(context.Background(), ref, typedReceiver) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + // IsAvailable checks if both conditions ContainersReady and PodReady of a Pod are true. func IsPodReady(pod corev1.Pod) bool { conditionsTrue := 0 diff --git a/pkg/utils/k8s/k8sutils_test.go b/pkg/utils/k8s/k8sutils_test.go index fd5c5bec22..159fc25e73 100644 --- a/pkg/utils/k8s/k8sutils_test.go +++ b/pkg/utils/k8s/k8sutils_test.go @@ -230,3 +230,54 @@ func TestCompareStorageRequests(t *testing.T) { }) } } + +func TestObjectExists(t *testing.T) { + type args struct { + c Client + ref types.NamespacedName + typedReceiver client.Object + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "existing secret", + args: args{ + c: NewFakeClient( + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "secret-name"}}, + ), + ref: types.NamespacedName{Namespace: "ns", Name: "secret-name"}, + typedReceiver: &corev1.Secret{}, + }, + want: true, + wantErr: false, + }, + { + name: "non-existing secret", + args: args{ + c: NewFakeClient( + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "secret-name"}}, + ), + ref: types.NamespacedName{Namespace: "ns", Name: "another-secret-name"}, + typedReceiver: &corev1.Secret{}, + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ObjectExists(tt.args.c, tt.args.ref, tt.args.typedReceiver) + if (err != nil) != tt.wantErr { + t.Errorf("ObjectExists() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ObjectExists() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/e2e/kb/association_test.go b/test/e2e/kb/association_test.go index 350808bb4c..581e896fd3 100644 --- a/test/e2e/kb/association_test.go +++ b/test/e2e/kb/association_test.go @@ -11,10 +11,13 @@ import ( "fmt" "testing" + "github.com/elastic/cloud-on-k8s/test/e2e/test/enterprisesearch" + commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/common/annotation" "github.com/elastic/cloud-on-k8s/pkg/controller/common/events" + "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/test/e2e/test" "github.com/elastic/cloud-on-k8s/test/e2e/test/elasticsearch" @@ -41,6 +44,39 @@ func TestCrossNSAssociation(t *testing.T) { test.Sequence(nil, test.EmptySteps, esBuilder, kbBuilder).RunSequential(t) } +// TestEntSearchAssociation tests associating Kibana to both Elasticsearch and Enterprise Search. +// Elasticsearch and Kibana run in the same namespace while Enterprise Search runs in a different one. +func TestEntSearchAssociation(t *testing.T) { + name := "test-kb-ent-assoc" + + // Kibana <-> EnterpriseSearch association is supported starting 7.14.0 + stackVersion := version.MustParse(test.Ctx().ElasticStackVersion) + if !stackVersion.GTE(version.MustParse("7.14.0-SNAPSHOT")) { + t.SkipNow() + } + + esKbNamespace := test.Ctx().ManagedNamespace(0) + entNamespace := test.Ctx().ManagedNamespace(1) + + esBuilder := elasticsearch.NewBuilder(name). + WithNamespace(esKbNamespace). + WithESMasterDataNodes(1, elasticsearch.DefaultResources). + WithRestrictedSecurityContext() + entBuilder := enterprisesearch.NewBuilder(name). + WithNamespace(entNamespace). + WithNodeCount(1). + WithElasticsearchRef(esBuilder.Ref()). + WithRestrictedSecurityContext() + kbBuilder := kibana.NewBuilder(name). + WithNamespace(esKbNamespace). + WithElasticsearchRef(esBuilder.Ref()). + WithEnterpriseSearchRef(entBuilder.Ref()). + WithNodeCount(1). + WithRestrictedSecurityContext() + + test.Sequence(nil, test.EmptySteps, esBuilder, entBuilder, kbBuilder).RunSequential(t) +} + func TestKibanaAssociationWithNonExistentES(t *testing.T) { name := "test-kb-assoc-non-existent-es" kbBuilder := kibana.NewBuilder(name). diff --git a/test/e2e/test/enterprisesearch/builder.go b/test/e2e/test/enterprisesearch/builder.go index 1868d80a26..0174aced85 100644 --- a/test/e2e/test/enterprisesearch/builder.go +++ b/test/e2e/test/enterprisesearch/builder.go @@ -81,6 +81,13 @@ func newBuilder(name, randSuffix string) Builder { WithPodLabel(run.TestNameLabel, name) } +func (b Builder) Ref() commonv1.ObjectSelector { + return commonv1.ObjectSelector{ + Name: b.EnterpriseSearch.Name, + Namespace: b.EnterpriseSearch.Namespace, + } +} + func (b Builder) WithSuffix(suffix string) Builder { if suffix != "" { b.EnterpriseSearch.ObjectMeta.Name = b.EnterpriseSearch.ObjectMeta.Name + "-" + suffix diff --git a/test/e2e/test/enterprisesearch/http_client.go b/test/e2e/test/enterprisesearch/http_client.go index 65dcacb6ba..1fba02d1a2 100644 --- a/test/e2e/test/enterprisesearch/http_client.go +++ b/test/e2e/test/enterprisesearch/http_client.go @@ -18,7 +18,6 @@ import ( entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch" - entName "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/test/e2e/test" ) @@ -39,7 +38,7 @@ func NewEnterpriseSearchClient(ent entv1.EnterpriseSearch, k *test.K8sClient) (E scheme := "http" if ent.Spec.HTTP.TLS.Enabled() { scheme = "https" - crts, err := k.GetHTTPCerts(entName.EntNamer, ent.Namespace, ent.Name) + crts, err := k.GetHTTPCerts(entv1.Namer, ent.Namespace, ent.Name) if err != nil { return EnterpriseSearchClient{}, err } diff --git a/test/e2e/test/enterprisesearch/steps_deletion.go b/test/e2e/test/enterprisesearch/steps_deletion.go index 3f4bfff302..3dfafd21b3 100644 --- a/test/e2e/test/enterprisesearch/steps_deletion.go +++ b/test/e2e/test/enterprisesearch/steps_deletion.go @@ -7,8 +7,8 @@ package enterprisesearch import ( "context" + entv1 "github.com/elastic/cloud-on-k8s/pkg/apis/enterprisesearch/v1" "github.com/elastic/cloud-on-k8s/pkg/controller/common/certificates" - "github.com/elastic/cloud-on-k8s/pkg/controller/enterprisesearch/name" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/test/e2e/test" "github.com/pkg/errors" @@ -57,7 +57,7 @@ func (b Builder) DeletionTestSteps(k *test.K8sClient) test.StepList { Test: test.Eventually(func() error { namespace := b.EnterpriseSearch.Namespace return k.CheckSecretsRemoved([]types.NamespacedName{ - {Namespace: namespace, Name: certificates.PublicCertsSecretName(name.EntNamer, b.EnterpriseSearch.Name)}, + {Namespace: namespace, Name: certificates.PublicCertsSecretName(entv1.Namer, b.EnterpriseSearch.Name)}, }) }), }, diff --git a/test/e2e/test/kibana/builder.go b/test/e2e/test/kibana/builder.go index 33410160d0..f7521c9075 100644 --- a/test/e2e/test/kibana/builder.go +++ b/test/e2e/test/kibana/builder.go @@ -82,6 +82,11 @@ func (b Builder) WithElasticsearchRef(ref commonv1.ObjectSelector) Builder { return b } +func (b Builder) WithEnterpriseSearchRef(ref commonv1.ObjectSelector) Builder { + b.Kibana.Spec.EnterpriseSearchRef = ref + return b +} + func (b Builder) WithExternalElasticsearchRef(ref commonv1.ObjectSelector) Builder { b.ExternalElasticsearchRef = ref return b @@ -205,5 +210,5 @@ func (b Builder) ElasticsearchRef() commonv1.ObjectSelector { return b.ExternalElasticsearchRef } // if no external Elasticsearch cluster is defined, use the ElasticsearchRef - return b.Kibana.AssociationRef() + return b.Kibana.EsAssociation().AssociationRef() } diff --git a/test/e2e/test/kibana/checks_k8s.go b/test/e2e/test/kibana/checks_k8s.go index af95af3290..78fb6f4e7d 100644 --- a/test/e2e/test/kibana/checks_k8s.go +++ b/test/e2e/test/kibana/checks_k8s.go @@ -107,6 +107,8 @@ func CheckStatus(b Builder, k *test.K8sClient) test.Step { } // don't check the association status that may vary across tests kb.Status.AssociationStatus = "" + kb.Status.ElasticsearchAssociationStatus = "" + kb.Status.EnterpriseSearchAssociationStatus = "" expected := kbv1.KibanaStatus{ DeploymentStatus: commonv1.DeploymentStatus{ AvailableNodes: b.Kibana.Spec.Count, diff --git a/test/e2e/test/kibana/checks_kb.go b/test/e2e/test/kibana/checks_kb.go index a9791d2e77..2d1445aba8 100644 --- a/test/e2e/test/kibana/checks_kb.go +++ b/test/e2e/test/kibana/checks_kb.go @@ -32,9 +32,14 @@ func (b Builder) CheckStackTestSteps(k *test.K8sClient) test.StepList { checks := kbChecks{ client: k, } - return test.StepList{ + tests := test.StepList{ checks.CheckKbStatusHealthy(b), } + if b.Kibana.Spec.EnterpriseSearchRef.IsDefined() { + tests = append(tests, checks.CheckEntSearchAccess(b)) + } + + return tests } // CheckKbStatusHealthy checks that Kibana is able to connect to Elasticsearch by inspecting its API status. @@ -62,3 +67,20 @@ func (check *kbChecks) CheckKbStatusHealthy(b Builder) test.Step { }), } } + +// CheckEntSearchAccess checks that the Enterprise Search UI is accessible in Kibana. +func (check *kbChecks) CheckEntSearchAccess(b Builder) test.Step { + return test.Step{ + Name: "The Enterprise Search UI should be available in Kibana", + Test: test.Eventually(func() error { + password, err := check.client.GetElasticPassword(b.ElasticsearchRef().NamespacedName()) + if err != nil { + return errors.Wrap(err, "while getting elastic password") + } + // returns 200 OK if accessible + path := "/api/enterprise_search/config_data" + _, err = DoRequest(check.client, b.Kibana, password, "GET", path, nil) + return err + }), + } +}