Skip to content
3 changes: 3 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,12 @@ admission policy installation; once an Installation exists it is the authority o
}

elasticIsMigrating := false
useSingleIndex := false
useExternalElastic := discovery.UseExternalElastic(bootConfig)

if isCloudBuild() {
elasticIsMigrating = discovery.ElasticIsMigrating(bootConfig)
useSingleIndex = discovery.UseSingleIndex(bootConfig)
if !elasticIsMigrating {
if err := verifyElasticSearch(ctx, cs, useExternalElastic); err != nil {
setupLog.Error(err, "Elasticsearch configuration verification failed")
Expand Down Expand Up @@ -621,6 +623,7 @@ admission policy installation; once an Installation exists it is the authority o
ElasticExternal: useExternalElastic,
Cloud: isCloudBuild(),
ESMigration: elasticIsMigrating,
UseSingleIndex: useSingleIndex,
UseV3CRDs: v3CRDs,
APIDiscovery: apiDiscovery,
}
Expand Down
16 changes: 16 additions & 0 deletions pkg/common/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,19 @@ func ElasticIsMigrating(config *corev1.ConfigMap) bool {
}
return false
}

// UseSingleIndex returns true if this cluster is in the last phase of a migration to single-index
// storage, during which the operator must reconfigure Linseed to use the single-index names.
func UseSingleIndex(config *corev1.ConfigMap) bool {
Comment on lines +310 to +312

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment suggests that this function is only applicable for migration, but the name of the function sounds more general. Perhaps this would be better named "SingleIndexMigrationDone" or similar? Or is the comment just overly specific?

if config == nil {
return false
}

// Load the operator bootstrap configuration from its configmap.
if val, ok := config.Data["USE_SINGLE_INDEX"]; ok && val != "" {
if strings.ToLower(val) == "true" {
return true
}
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,7 @@ func (d DashboardsSubController) Reconcile(ctx context.Context, request reconcil
}

// Query the username and password this Dashboards Installer instance should use to authenticate with Elasticsearch.
// For multi-tenant systems, credentials are created by the elasticsearch users controller.
// For single-tenant system, these are created by es-kube-controllers.
// For cloud, credentials are created by the elasticsearch users controller.
key = types.NamespacedName{Name: dashboards.ElasticCredentialsSecret, Namespace: helper.InstallNamespace()}
credentials := corev1.Secret{}
if err = d.client.Get(ctx, key, &credentials); err != nil && !errors.IsNotFound(err) {
Expand Down
33 changes: 32 additions & 1 deletion pkg/controller/logstorage/initializer/conditions_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ package initializer
import (
"context"
"fmt"
"sort"
"time"

"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -44,6 +46,7 @@ func AddConditionsController(mgr manager.Manager, opts options.ControllerOptions
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
multiTenant: opts.MultiTenant,
cloud: opts.Cloud,
}

return ctrl.NewControllerManagedBy(mgr).
Expand All @@ -62,6 +65,10 @@ type LogStorageConditions struct {
client client.Client
scheme *runtime.Scheme
multiTenant bool

// cloud indicates that this is a Calico Cloud install, in which case the log-storage users
// controller runs in single-tenant mode too and reports status.
cloud bool
}

func (r *LogStorageConditions) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
Expand All @@ -88,9 +95,22 @@ func (r *LogStorageConditions) Reconcile(ctx context.Context, request reconcile.
}

// Compare and update the current StatusCondition if there are any new changes
ls.Status.Conditions = updateConditions(currentConditions, desiredConditions)
conditions := updateConditions(currentConditions, desiredConditions)

// Skip the write if nothing changed. This controller watches LogStorage, so a no-op write would
// re-trigger it and spin: each reconcile would bump the resourceVersion and enqueue another one.
if equality.Semantic.DeepEqual(ls.Status.Conditions, conditions) {
return reconcile.Result{}, nil
}
Comment on lines +100 to +104

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unnecessary if we're doing the sorting below - the k8s API server will notice that nothing has changed, and won't update the generation nor send an update. So I thinkwe can remove this check (not that it's a problem really, but it's another place something could go wrong if the equality check is not correct).

ls.Status.Conditions = conditions

if err := r.client.Status().Update(ctx, ls); err != nil {
if errors.IsConflict(err) {
// The LogStorage was modified after we read it - our cached copy is stale. Requeue and
// recompute the conditions from the updated object instead of reporting an error.
reqLogger.V(3).Info("Conflict updating LogStorage status conditions, retrying")
return reconcile.Result{Requeue: true}, nil
}
log.WithValues("reason", err).Info("Failed to update LogStorage status conditions")
return reconcile.Result{}, err
}
Expand All @@ -112,6 +132,10 @@ func (r *LogStorageConditions) getDesiredConditions(ctx context.Context) (map[st
expectedInstances = append(expectedInstances, TigeraStatusLogStorageUsers)
} else {
expectedInstances = append(expectedInstances, TigeraStatusLogStorageESMetrics, TigeraStatusLogStorageKubeController, TigeraStatusLogStorageDashboards)
if r.cloud {
// In Calico Cloud, the users controller runs in single-tenant mode too.
expectedInstances = append(expectedInstances, TigeraStatusLogStorageUsers)
}
}

// Keep track of which instances are in which state.
Expand Down Expand Up @@ -197,5 +221,12 @@ func updateConditions(currentConditions, desiredConditions map[string]metav1.Con

statusConditions = append(statusConditions, desired)
}

// desiredConditions is a map, so iteration order is random. Sort by type to keep the stored
// conditions stable across reconciles - otherwise every write reorders the list, which counts
// as a change and triggers another reconcile.
sort.Slice(statusConditions, func(i, j int) bool {
return statusConditions[i].Type < statusConditions[j].Type
})
return statusConditions
}
68 changes: 29 additions & 39 deletions pkg/controller/logstorage/linseed/linseed_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,12 @@ import (
var log = logf.Log.WithName("controller_logstorage_linseed")

type LinseedSubController struct {
client client.Client
scheme *runtime.Scheme
status status.StatusManager
clusterDomain string
variant operatorv1.ProductVariant
tierWatchReady *utils.ReadyFlag
dpiAPIReady *utils.ReadyFlag
multiTenant bool
elasticExternal bool
cloud bool
client client.Client
scheme *runtime.Scheme
status status.StatusManager
tierWatchReady *utils.ReadyFlag
dpiAPIReady *utils.ReadyFlag
opts options.ControllerOptions
}

func Add(mgr manager.Manager, opts options.ControllerOptions) error {
Expand All @@ -79,16 +75,12 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {

// Create the reconciler
r := &LinseedSubController{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
clusterDomain: opts.ClusterDomain,
variant: opts.Variant,
tierWatchReady: &utils.ReadyFlag{},
dpiAPIReady: &utils.ReadyFlag{},
multiTenant: opts.MultiTenant,
status: status.New(mgr.GetClient(), "log-storage-access", opts.KubernetesVersion),
elasticExternal: opts.ElasticExternal,
cloud: opts.Cloud,
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
tierWatchReady: &utils.ReadyFlag{},
dpiAPIReady: &utils.ReadyFlag{},
status: status.New(mgr.GetClient(), "log-storage-access", opts.KubernetesVersion),
opts: opts,
}
r.status.Run(opts.ShutdownContext)

Expand Down Expand Up @@ -200,19 +192,19 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
}

func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
helper := utils.NewNamespaceHelper(r.multiTenant, render.ElasticsearchNamespace, request.Namespace)
helper := utils.NewNamespaceHelper(r.opts.MultiTenant, render.ElasticsearchNamespace, request.Namespace)
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name, "installNS", helper.InstallNamespace(), "truthNS", helper.TruthNamespace())
reqLogger.Info("Reconciling LogStorage - Linseed")

// We skip requests without a namespace specified in multi-tenant setups.
if r.multiTenant && request.Namespace == "" {
if r.opts.MultiTenant && request.Namespace == "" {
return reconcile.Result{}, nil
}

// When running in multi-tenant mode, we need to install Linseed in tenant Namespaces. However, the LogStorage
// resource is still cluster-scoped (since ES is a cluster-wide resource), so we need to look elsewhere to determine
// which tenant namespaces require a Linseed instance. We use the tenant API to determine the set of namespaces that should have a Linseed.
tenant, _, err := utils.GetTenant(ctx, r.multiTenant, r.client, request.Namespace)
tenant, _, err := utils.GetTenant(ctx, r.opts.MultiTenant, r.client, request.Namespace)
if errors.IsNotFound(err) {
reqLogger.Info("No Tenant in this Namespace, skip")
return reconcile.Result{}, nil
Expand Down Expand Up @@ -310,7 +302,7 @@ func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.
elasticHost := "tigera-secure-es-http.tigera-elasticsearch.svc"
elasticPort := "9200"
var esClientSecret *corev1.Secret
if !r.elasticExternal {
if !r.opts.ElasticExternal {
// Wait for Elasticsearch to be installed and available.
elasticsearch, err := utils.GetElasticsearch(ctx, r.client)
if err != nil {
Expand All @@ -322,21 +314,21 @@ func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.
return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil
}
} else {
if r.multiTenant {
if r.opts.MultiTenant {
// The Tenant resource must specify the ES endpoint.
if tenant == nil || tenant.Spec.Elastic == nil || tenant.Spec.Elastic.URL == "" {
reqLogger.Error(nil, "Elasticsearch URL must be specified for this tenant")
r.status.SetDegraded(operatorv1.ResourceValidationError, "Elasticsearch URL must be specified for this tenant", nil, reqLogger)
return reconcile.Result{}, nil
}
} else if r.cloud {
} else if r.opts.Cloud {
// Calico Cloud single-tenant: there is no Tenant CR, so read it from the cloud config map.
cloudConfig, err := utils.GetCloudConfig(ctx, r.client)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Failed to read cloud config", err, reqLogger)
return reconcile.Result{}, err
}
tenant = cloudConfig.ToTenant()
tenant = cloudConfig.ToTenant(cloudconfig.WithStandardIndicesIf(r.opts.UseSingleIndex))
}

// Determine the host and port from the URL.
Expand All @@ -362,8 +354,7 @@ func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.
}

// Query the username and password this Linseed instance should use to authenticate with Elasticsearch.
// For multi-tenant systems, credentials are created by the elasticsearch users controller.
// For single-tenant system, these are created by es-kube-controllers.
// For cloud systems, credentials are created by the elasticsearch users controller.
key = types.NamespacedName{Name: render.ElasticsearchLinseedUserSecret, Namespace: helper.InstallNamespace()}
credentials := corev1.Secret{}
if err = r.client.Get(ctx, key, &credentials); err != nil && !errors.IsNotFound(err) {
Expand All @@ -379,12 +370,12 @@ func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.
certificatemanager.WithLogger(reqLogger),
certificatemanager.WithTenant(tenant),
}
cm, err := certificatemanager.Create(r.client, installationSpec, r.clusterDomain, helper.TruthNamespace(), opts...)
cm, err := certificatemanager.Create(r.client, installationSpec, r.opts.ClusterDomain, helper.TruthNamespace(), opts...)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceCreateError, "Unable to create the Tigera CA", err, reqLogger)
return reconcile.Result{}, err
}
linseedDNSNames := dns.GetServiceDNSNames(render.LinseedServiceName, helper.InstallNamespace(), r.clusterDomain)
linseedDNSNames := dns.GetServiceDNSNames(render.LinseedServiceName, helper.InstallNamespace(), r.opts.ClusterDomain)
linseedKeyPair, err := cm.GetKeyPair(r.client, render.TigeraLinseedSecret, helper.TruthNamespace(), linseedDNSNames)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error getting Linseed KeyPair", err, reqLogger)
Expand Down Expand Up @@ -419,10 +410,8 @@ func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.
}

// Query the username and password this Linseed instance should use to authenticate with Elasticsearch.
// For multi-tenant systems, credentials are created by the elasticsearch users controller.
// For single-tenant system, these are created by es-kube-controllers.
// For cloud systems, credentials are created by the elasticsearch users controller.
// Delay installing Linseed until available.
// TODO: Switch single-tenant to using operator-provisioned users.
key = types.NamespacedName{Name: render.ElasticsearchLinseedUserSecret, Namespace: helper.InstallNamespace()}
if err = r.client.Get(ctx, key, &corev1.Secret{}); err != nil && !errors.IsNotFound(err) {
r.status.SetDegraded(operatorv1.ResourceReadError, fmt.Sprintf("Error getting Secret %s", key), err, reqLogger)
Expand Down Expand Up @@ -456,31 +445,32 @@ func (r *LinseedSubController) Reconcile(ctx context.Context, request reconcile.
Namespace: helper.InstallNamespace(),
BindNamespaces: bindNamespaces,
TrustedBundle: trustedBundle,
ClusterDomain: r.clusterDomain,
ClusterDomain: r.opts.ClusterDomain,
KeyPair: linseedKeyPair,
TokenKeyPair: tokenKeyPair,
ESClusterConfig: esClusterConfig,
HasDPIResource: hasDPIResource,
ManagementCluster: managementCluster != nil,
Tenant: tenant,
ExternalElastic: r.elasticExternal,
ExternalElastic: r.opts.ElasticExternal,
ElasticHost: elasticHost,
ElasticPort: elasticPort,
ElasticClientSecret: esClientSecret,
ElasticClientCredentialsSecret: &credentials,
LogStorage: logStorage,
Cloud: r.cloud,
Cloud: r.opts.Cloud,
UseSingleIndex: r.opts.UseSingleIndex,
}
linseedComponent := linseed.Linseed(cfg)

if err := imageset.ApplyImageSet(ctx, r.client, r.variant, linseedComponent); err != nil {
if err := imageset.ApplyImageSet(ctx, r.client, r.opts.Variant, linseedComponent); err != nil {
r.status.SetDegraded(operatorv1.ResourceUpdateError, "Error with images from ImageSet", err, reqLogger)
return reconcile.Result{}, err
}

// In standard installs, the LogStorage owns Linseed. For multi-tenant, it's owned by the Tenant instance.
var hdler utils.ComponentHandler
if r.multiTenant {
if r.opts.MultiTenant {
hdler = utils.NewComponentHandler(reqLogger, r.client, r.scheme, tenant)
} else {
hdler = utils.NewComponentHandler(reqLogger, r.client, r.scheme, logStorage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,9 @@ func NewLinseedControllerWithShims(
client: cli,
scheme: scheme,
status: status,
clusterDomain: opts.ClusterDomain,
variant: opts.Variant,
multiTenant: opts.MultiTenant,
tierWatchReady: &utils.ReadyFlag{},
dpiAPIReady: &utils.ReadyFlag{},
opts: opts,
}
r.tierWatchReady.MarkAsReady()
r.dpiAPIReady.MarkAsReady()
Expand Down Expand Up @@ -516,8 +514,8 @@ var _ = Describe("LogStorage Linseed controller", func() {
Expect(cli.Delete(ctx, es)).ShouldNot(HaveOccurred())

// Set the reconcile to run in external ES mode.
r.elasticExternal = true
r.multiTenant = true
r.opts.ElasticExternal = true
r.opts.MultiTenant = true

// Set the elasticsearch configuration for the tenant.
tenant.Spec.Elastic = &operatorv1.TenantElasticSpec{URL: "https://external.elastic:443"}
Expand Down
Loading
Loading