Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg/controller/installation/core_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ var _ = Describe("Testing core-controller installation", func() {
})
})

It("degrades with a validation reason when the extension rejects the configuration", func() {
It("degrades with a configuration reason when the extension rejects the configuration", func() {
mockStatus.On("SetDegraded", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()

port := 0
Expand All @@ -410,7 +410,7 @@ var _ = Describe("Testing core-controller installation", func() {

_, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).To(HaveOccurred())
mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.ResourceValidationError, "felixConfiguration prometheusReporterPort=0 not supported", mock.Anything, mock.Anything)
mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.InvalidConfigurationError, "invalid metrics port: felixConfiguration prometheusReporterPort=0 not supported", mock.Anything, mock.Anything)
})

Context("image tests", func() {
Expand Down
34 changes: 17 additions & 17 deletions pkg/enterprise/apiserver/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,22 +182,22 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con

trustedBundle, err := ci.CertificateManager.CreateNamedTrustedBundleFromSecrets(render.APIServerResourceName, ci.Client, common.OperatorNamespace(), false)
if err != nil {
return ci, nil, fmt.Errorf("unable to create the trusted bundle: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "unable to create the trusted bundle: %w", err)
}

applicationLayer, err := utils.GetApplicationLayer(ctx, ci.Client)
if err != nil {
return ci, nil, fmt.Errorf("error reading ApplicationLayer: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ApplicationLayer: %w", err)
}

managementCluster, err := utils.GetManagementCluster(ctx, ci.Client)
if err != nil {
return ci, nil, fmt.Errorf("error reading ManagementCluster: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementCluster: %w", err)
}

managementClusterConnection, err := utils.GetManagementClusterConnection(ctx, ci.Client)
if err != nil {
return ci, nil, fmt.Errorf("error reading ManagementClusterConnection: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementClusterConnection: %w", err)
}

if managementCluster != nil && managementClusterConnection != nil {
Expand All @@ -206,21 +206,21 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con

rbacManagementEnabled, err := utils.RBACManagementEnabled(ctx, ci.Client, e.variant, e.opts.MultiTenant)
if err != nil {
return ci, nil, fmt.Errorf("error reading the RBAC management UI ConfigMap: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading the RBAC management UI ConfigMap: %w", err)
}

// Management cluster only: the apiserver mounts the tunnel CA secret so it can sign
// certificates for managed clusters. The manager controller writes it once
// ManagementCluster.Spec.TLS is defaulted; degrade until it exists.
if managementCluster != nil && managementCluster.Spec.TLS != nil && !e.opts.MultiTenant {
if _, err := utils.GetSecret(ctx, ci.Client, managementCluster.Spec.TLS.SecretName, common.OperatorNamespace()); err != nil {
return ci, nil, fmt.Errorf("unable to fetch the tunnel secret: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "unable to fetch the tunnel secret: %w", err)
}
}

prometheusCertificate, err := ci.CertificateManager.GetCertificate(ci.Client, monitor.PrometheusClientTLSSecretName, common.OperatorNamespace())
if err != nil {
return ci, nil, fmt.Errorf("failed to get certificate: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to get certificate: %w", err)
}
if prometheusCertificate != nil {
trustedBundle.AddCertificates(prometheusCertificate)
Expand All @@ -229,7 +229,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
if managementClusterConnection != nil {
voltronLinseedCert, err := ci.CertificateManager.GetCertificate(ci.Client, render.VoltronLinseedPublicCert, common.OperatorNamespace())
if err != nil {
return ci, nil, fmt.Errorf("failed to retrieve %s: %w", render.VoltronLinseedPublicCert, err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to retrieve %s: %w", render.VoltronLinseedPublicCert, err)
}
if voltronLinseedCert != nil {
trustedBundle.AddCertificates(voltronLinseedCert)
Expand All @@ -241,21 +241,21 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
var keyValidatorConfig authentication.KeyValidatorConfig
authenticationCR, err := utils.GetAuthentication(ctx, ci.Client)
if err != nil && !apierrors.IsNotFound(err) {
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error while fetching Authentication: %s", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error while fetching Authentication: %w", err)
}
if authenticationCR != nil && authenticationCR.Status.State == operatorv1.TigeraStatusReady {
if utils.DexEnabled(authenticationCR) {
certificate, err := ci.CertificateManager.GetCertificate(ci.Client, render.DexTLSSecretName, common.OperatorNamespace())
if err != nil {
return ci, nil, extensions.Degradedf(operatorv1.CertificateError, "failed to retrieve %s: %s", render.DexTLSSecretName, err)
return ci, nil, extensions.Degradedf(operatorv1.CertificateError, "failed to retrieve %s: %w", render.DexTLSSecretName, err)
} else if certificate == nil {
return ci, nil, extensions.NotReadyf("waiting for secret '%s' to become available", render.DexTLSSecretName)
}
trustedBundle.AddCertificates(certificate)
}
keyValidatorConfig, err = utils.GetKeyValidatorConfig(ctx, ci.Client, authenticationCR, ci.RenderInputs.ClusterDomain, false)
if err != nil {
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to get KeyValidator config: %s", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to get KeyValidator config: %w", err)
}
}

Expand All @@ -270,7 +270,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
dns.GetServiceDNSNames(render.APIServerServiceName, render.APIServerNamespace, ci.RenderInputs.ClusterDomain),
)
if err != nil {
return ci, nil, fmt.Errorf("unable to get or create query server tls key pair: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "unable to get or create query server tls key pair: %w", err)
}
}

Expand All @@ -279,11 +279,11 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
// sidecar images are only needed when sidecar injection is enabled.
imageSet, err := imageset.GetImageSet(ctx, ci.Client, in.Variant)
if err != nil {
return ci, nil, err
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error getting ImageSet: %w", err)
}
calicoImage, err := components.GetReference(components.CombinedCalicoImage(in), in.Registry, in.ImagePath, in.ImagePrefix, imageSet)
if err != nil {
return ci, nil, err
return ci, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err)
}

var l7EnvoyImage, dikastesImage string
Expand All @@ -292,11 +292,11 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
*applicationLayer.Spec.SidecarInjection == operatorv1.SidecarEnabled {
l7EnvoyImage, err = components.GetReference(components.ComponentEnvoyProxy, in.Registry, in.ImagePath, in.ImagePrefix, imageSet)
if err != nil {
return ci, nil, err
return ci, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err)
}
dikastesImage, err = components.GetReference(components.ComponentDikastes, in.Registry, in.ImagePath, in.ImagePrefix, imageSet)
if err != nil {
return ci, nil, err
return ci, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err)
}
}

Expand All @@ -308,7 +308,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
if e.opts.MultiTenant {
bindingNamespaces, err = utils.TenantNamespaces(ctx, ci.Client, nil)
if err != nil {
return ci, nil, fmt.Errorf("error reading tenant namespaces: %w", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading tenant namespaces: %w", err)
}
}

Expand Down
57 changes: 55 additions & 2 deletions pkg/enterprise/apiserver/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package apiserver_test
import (
"context"
"fmt"
"reflect"
"slices"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -47,6 +48,7 @@ import (
"github.com/tigera/operator/pkg/extensions/extensionstest"
"github.com/tigera/operator/pkg/render"
"github.com/tigera/operator/pkg/render/common/rbacmanagement"
"github.com/tigera/operator/pkg/render/monitor"
"github.com/tigera/operator/pkg/tls/certificatemanagement"
)

Expand Down Expand Up @@ -93,15 +95,35 @@ func apiServerScheme() *runtime.Scheme {
// failingConfigMapInputs returns controller inputs whose client fails every read of
// the named ConfigMap with readErr.
func failingConfigMapInputs(name string, readErr error) controller.Inputs {
return failingGetInputs(failingGet{obj: &corev1.ConfigMap{}, name: name}, readErr)
}

// failingGet names the object read that should fail: any object of the same type as
// obj, narrowed to a single name when one is given.
type failingGet struct {
obj client.Object
name string
}

func (f failingGet) matches(key client.ObjectKey, obj client.Object) bool {
if reflect.TypeOf(obj) != reflect.TypeOf(f.obj) {
return false
}
return f.name == "" || f.name == key.Name
}

// failingGetInputs returns controller inputs whose client fails the read fail
// describes with readErr.
func failingGetInputs(fail failingGet, readErr error, objs ...client.Object) controller.Inputs {
c := ctrlrfake.DefaultFakeClientBuilder(apiServerScheme()).WithInterceptorFuncs(interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if _, ok := obj.(*corev1.ConfigMap); ok && key.Name == name {
if fail.matches(key, obj) {
return readErr
}
return c.Get(ctx, key, obj, opts...)
},
}).Build()
return apiServerControllerInputsWith(c, operatorv1.CalicoEnterprise, nil)
return apiServerControllerInputsWith(c, operatorv1.CalicoEnterprise, nil, objs...)
}

// rbacManagementGate builds the admin-owned ConfigMap that switches the RBAC
Expand Down Expand Up @@ -173,6 +195,34 @@ var _ = Describe("API server enterprise controller extension", func() {
})
})

DescribeTable("reports the degraded reason for the read it failed on",
func(fail failingGet, expected operatorv1.TigeraStatusReason, objs ...client.Object) {
readErr := fmt.Errorf("the API server is having a bad day")

_, _, err := ext.APIServer().ExtendInputs(ctx, failingGetInputs(fail, readErr, objs...))
Expect(err).To(MatchError(readErr))
reason, ok := extensions.DegradedReason(err)
Expect(ok).To(BeTrue())
Expect(reason).To(Equal(expected))
},
Entry("the ApplicationLayer",
failingGet{obj: &operatorv1.ApplicationLayer{}}, operatorv1.ResourceReadError),
Entry("the ManagementCluster",
failingGet{obj: &operatorv1.ManagementCluster{}}, operatorv1.ResourceReadError),
Entry("the ManagementClusterConnection",
failingGet{obj: &operatorv1.ManagementClusterConnection{}}, operatorv1.ResourceReadError),
Entry("the tunnel secret on a management cluster",
failingGet{obj: &corev1.Secret{}, name: render.VoltronTunnelSecretName}, operatorv1.ResourceReadError,
managementCluster()),
Entry("the prometheus client certificate",
failingGet{obj: &corev1.Secret{}, name: monitor.PrometheusClientTLSSecretName}, operatorv1.ResourceReadError),
Entry("the Voltron linseed certificate on a managed cluster",
failingGet{obj: &corev1.Secret{}, name: render.VoltronLinseedPublicCert}, operatorv1.ResourceReadError,
managementClusterConnection()),
Entry("the Authentication",
failingGet{obj: &operatorv1.Authentication{}}, operatorv1.ResourceReadError),
)

Describe("Dex", func() {
readyAuthentication := func() *operatorv1.Authentication {
return &operatorv1.Authentication{
Expand Down Expand Up @@ -380,6 +430,9 @@ var _ = Describe("API server enterprise modifier", func() {

_, _, err := ext.APIServer().ExtendInputs(ctx, ci)
Expect(err).To(MatchError(readErr))
reason, ok := extensions.DegradedReason(err)
Expect(ok).To(BeTrue())
Expect(reason).To(Equal(operatorv1.ResourceReadError))
})

Context("Calico Cloud", func() {
Expand Down
7 changes: 3 additions & 4 deletions pkg/enterprise/clusterconnection/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package clusterconnection

import (
"context"
"fmt"

k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -103,7 +102,7 @@ func (e *Extension) ValidateAndDefault(cr *operatorv1.ManagementClusterConnectio
func (e *Extension) validate(ctx context.Context, ci controller.Inputs) error {
managementCluster, err := utils.GetManagementCluster(ctx, ci.Client)
if err != nil {
return fmt.Errorf("error reading ManagementCluster: %w", err)
return extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementCluster: %w", err)
}
if managementCluster != nil {
return extensions.InvalidConfigf("having both a ManagementCluster and a ManagementClusterConnection is not supported")
Expand All @@ -123,7 +122,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con

clusterInformation, err := utils.FetchClusterInformation(ctx, ci.Client)
if err != nil {
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying ClusterInformation: %s", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying ClusterInformation: %w", err)
}

// Ensure the license can support enterprise policy before enabling the
Expand All @@ -132,7 +131,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con
if license, err := utils.FetchLicenseKey(ctx, ci.Client); err == nil {
includeEgressNetworkPolicy = utils.IsFeatureActive(license, common.EgressAccessControlFeature)
} else if !k8serrors.IsNotFound(err) {
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying license: %s", err)
return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying license: %w", err)
}

ci.RenderInputs.Extension = render.GuardianRenderData{
Expand Down
30 changes: 30 additions & 0 deletions pkg/enterprise/clusterconnection/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
package clusterconnection_test

import (
"context"
"fmt"
"reflect"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"

v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"

Expand Down Expand Up @@ -60,6 +65,20 @@ var _ = Describe("clusterconnection enterprise controller extension", func() {
return ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(objs...).Build()
}

// failingGetClient fails every read of an object of the same type as fail.
failingGetClient := func(fail client.Object, readErr error) client.Client {
scheme := runtime.NewScheme()
Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred())
return ctrlrfake.DefaultFakeClientBuilder(scheme).WithInterceptorFuncs(interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if reflect.TypeOf(obj) == reflect.TypeOf(fail) {
return readErr
}
return c.Get(ctx, key, obj, opts...)
},
}).Build()
}

Describe("configuration", func() {
It("rejects a cluster that is both a management and a managed cluster", func() {
cli = newClient(&operatorv1.ManagementCluster{ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"}})
Expand All @@ -70,6 +89,17 @@ var _ = Describe("clusterconnection enterprise controller extension", func() {
Expect(err.Error()).To(ContainSubstring("not supported"))
})

It("degrades with a read error when the ManagementCluster cannot be read", func() {
readErr := fmt.Errorf("the API server is having a bad day")
cli = failingGetClient(&operatorv1.ManagementCluster{}, readErr)

_, _, err := ext.ClusterConnection().ExtendInputs(ctx, controllerInputs())
Expect(err).To(MatchError(readErr))
reason, ok := extensions.DegradedReason(err)
Expect(ok).To(BeTrue())
Expect(reason).To(Equal(operatorv1.ResourceReadError))
})

It("accepts impersonation and defaults it to empty lists", func() {
cr := &operatorv1.ManagementClusterConnection{}
Expect(ext.ClusterConnection().ValidateAndDefault(cr)).NotTo(HaveOccurred())
Expand Down
Loading
Loading