pkg/apis/common/v1/association.go (187 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package v1 import ( "fmt" "sort" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/hash" ) const ( singleStatusKey = "" ) // AssociationType is the type of an association resource, eg. for Kibana-ES association, AssociationType identifies ES. type AssociationType string // AssociationStatus is the status of an association resource. type AssociationStatus string // AssociationStatusMap is the map of association's namespaced name string to its AssociationStatus. For resources that // have a single Association of a given type (for ex. single ES reference), this map contains a single entry. type AssociationStatusMap map[string]AssociationStatus // NewSingleAssociationStatusMap creates an AssociationStatusMap that expects only a single Association. Using a // well-known key allows to keep serialization of the map backwards compatible. func NewSingleAssociationStatusMap(status AssociationStatus) AssociationStatusMap { return map[string]AssociationStatus{ singleStatusKey: status, } } func (asm AssociationStatusMap) String() string { // check if it's single status map and return only status string if yes // this allows to keep serialization backwards compatible if len(asm) == 1 { for key, value := range asm { if key == singleStatusKey { return string(value) } } } // sort by keys to make String() stable keys := make([]string, 0, len(asm)) for key := range asm { keys = append(keys, key) } sort.StringSlice(keys).Sort() var i int var sb strings.Builder for _, key := range keys { value := asm[key] i++ sb.WriteString(key + ": " + string(value)) if len(asm) != i { sb.WriteString(", ") } } return sb.String() } func (asm AssociationStatusMap) Single() (AssociationStatus, error) { if len(asm) > 1 { return "", fmt.Errorf("expected at most one AssociationStatus in map, but found %d: %s", len(asm), asm) } // returns the only AssociationStatus present or zero value if none are var result AssociationStatus for _, status := range asm { result = status } return result, nil } // AllEstablished returns true iff all Associations have AssociationEstablished status, false otherwise. func (asm AssociationStatusMap) AllEstablished() bool { for _, status := range asm { if status != AssociationEstablished { return false } } return true } const ( ElasticsearchConfigAnnotationNameBase = "association.k8s.elastic.co/es-conf" ElasticsearchAssociationType = "elasticsearch" EsMonitoringAssociationType = "es-monitoring" KibanaConfigAnnotationNameBase = "association.k8s.elastic.co/kb-conf" KibanaAssociationType = "kibana" KbMonitoringAssociationType = "kb-monitoring" EntConfigAnnotationNameBase = "association.k8s.elastic.co/ent-conf" EntAssociationType = "ent" FleetServerConfigAnnotationNameBase = "association.k8s.elastic.co/fs-conf" FleetServerAssociationType = "fleetserver" BeatConfigAnnotationNameBase = "association.k8s.elastic.co/beat-conf" BeatAssociationType = "beat" BeatMonitoringAssociationType = "beat-monitoring" LogstashMonitoringAssociationType = "ls-monitoring" AssociationUnknown AssociationStatus = "" AssociationPending AssociationStatus = "Pending" AssociationEstablished AssociationStatus = "Established" AssociationFailed AssociationStatus = "Failed" // SingletonAssociationID is an `AssociationID` used for Associations for resources that can have only a single // Association of each type. For example, Kibana can only have a single ES Association, so Kibana-ES Associations // 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 = "-" ) type ServiceAccountName string // Associated represents an Elastic stack resource that is associated with other stack resources. // Examples: // - Kibana can be associated with Elasticsearch // - APMServer can be associated with Elasticsearch and Kibana // - EnterpriseSearch can be associated with Elasticsearch // - Beat can be associated with Elasticsearch and Kibana // - Agent can be associated with multiple Elasticsearches // +kubebuilder:object:generate=false type Associated interface { metav1.Object runtime.Object ServiceAccountName() string GetAssociations() []Association AssociationStatusMap(typ AssociationType) AssociationStatusMap SetAssociationStatusMap(typ AssociationType, statusMap AssociationStatusMap) error } // Association interface helps to manage the Spec fields involved in an association. // +kubebuilder:object:generate=false type Association interface { Associated // ElasticServiceAccount returns the Elasticsearch service account name to be used for authentication. ElasticServiceAccount() (ServiceAccountName, error) // Associated can be used to retrieve the associated object Associated() Associated // AssociationType returns a string describing the type of the target resource (elasticsearch most of the time) // It is mostly used to build some other strings depending on the type of the targeted resource. AssociationType() AssociationType // AssociationRef is a reference to the associated resource. If defined with a Name then the Namespace is expected // to be set in the returned object. AssociationRef() ObjectSelector // AssociationConfAnnotationName is the name of the annotation used to define the config for the associated resource. // It is used by the association controller to store the configuration and by the controller which is // managing the associated resource to build the appropriate configuration. AssociationConfAnnotationName() string // AssociationConf is the configuration of the Association allowing to connect to the Association resource. AssociationConf() (*AssociationConf, error) SetAssociationConf(*AssociationConf) // SupportsAuthAPIKey returns true if the Association supports authenticating with an API key SupportsAuthAPIKey() bool // AssociationID uniquely identifies this Association among all Associations of the same type belonging to Associated() AssociationID() string } // FormatNameWithID conditionally formats `template`. `template` is expected to have a single %s verb. // If `id` is empty, the %s verb will be formatted with empty string. Otherwise %s verb will be replaced with `-id`. // Eg: // FormatNameWithID("name%s", "") returns "name" // FormatNameWithID("name%s", "ns1-es1") returns "name-ns1-es1" // FormatNameWithID("name%s", "ns2-es2") returns "name-ns2-es2" // This function exists to abstract this conditional logic away from the callers. It can be used to format names // for objects differing only by id, that would otherwise collide. In addition, it allows to preserve current naming // for object types with a single association and introduce object types with unbounded number of associations. func FormatNameWithID(template string, id string) string { if id != SingletonAssociationID { // we want names to be changed for any id but SingletonAssociationID id = fmt.Sprintf("-%s", id) } return fmt.Sprintf(template, id) } // AssociationConf holds the association configuration of a referenced resource in an association. type AssociationConf struct { AuthSecretName string `json:"authSecretName"` AuthSecretKey string `json:"authSecretKey"` IsAPIKey bool `json:"isApiKey"` IsServiceAccount bool `json:"isServiceAccount"` CACertProvided bool `json:"caCertProvided"` CASecretName string `json:"caSecretName"` // AdditionalSecretsHash is a hash of additional secrets such that when any of the underlying // secrets change, the CRD annotation is updated and the pods are restarted. AdditionalSecretsHash string `json:"additionalSecretsHash,omitempty"` URL string `json:"url"` // Version of the referenced resource. If a version upgrade is in progress, // matches the lowest running version. May be empty if unknown. Version string `json:"version"` // Serverless is true when the referenced resource is a serverless project. Serverless bool `json:"serverless,omitempty"` } // IsConfigured returns true if all the fields are set. func (ac *AssociationConf) IsConfigured() bool { if ac.GetCACertProvided() && !ac.CAIsConfigured() { return false } return ac.AuthIsConfigured() && ac.URLIsConfigured() } // 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 { return false } return ac.CASecretName != "" } // URLIsConfigured returns true if the URL field is set. func (ac *AssociationConf) URLIsConfigured() bool { if ac == nil { return false } return ac.URL != "" } func (ac *AssociationConf) GetAuthSecretName() string { if ac == nil { return "" } return ac.AuthSecretName } func (ac *AssociationConf) GetAuthSecretKey() string { if ac == nil { return "" } return ac.AuthSecretKey } func (ac *AssociationConf) GetCACertProvided() bool { if ac == nil { return false } return ac.CACertProvided } func (ac *AssociationConf) GetCASecretName() string { if ac == nil { return "" } return ac.CASecretName } func (ac *AssociationConf) GetURL() string { if ac == nil { return "" } return ac.URL } func ElasticsearchConfigAnnotationName(o ObjectSelector) string { // annotation key should be stable to allow the Elasticsearch Controller to only pick up the ones it expects, // based on the ObjectSelector return FormatNameWithID(ElasticsearchConfigAnnotationNameBase+"%s", hash.HashObject(o)) }