operator: Refactor CreateOrUpdateLokiStack handler (#11592)

Co-authored-by: Robert Jacob <xperimental@solidproject.de>
pull/11511/head^2
Periklis Tsirakidis 2 years ago committed by GitHub
parent edba360e43
commit 0065fd6e95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      operator/internal/handlers/internal/gateway/base_domain.go
  2. 87
      operator/internal/handlers/internal/gateway/gateway.go
  3. 390
      operator/internal/handlers/internal/gateway/gateway_test.go
  4. 3
      operator/internal/handlers/internal/gateway/modes.go
  5. 38
      operator/internal/handlers/internal/gateway/modes_test.go
  6. 8
      operator/internal/handlers/internal/gateway/tenant_configsecret.go
  7. 14
      operator/internal/handlers/internal/gateway/tenant_configsecret_test.go
  8. 12
      operator/internal/handlers/internal/gateway/tenant_secrets.go
  9. 10
      operator/internal/handlers/internal/gateway/tenant_secrets_test.go
  10. 39
      operator/internal/handlers/internal/rules/cleanup.go
  11. 223
      operator/internal/handlers/internal/rules/cleanup_test.go
  12. 7
      operator/internal/handlers/internal/rules/config.go
  13. 104
      operator/internal/handlers/internal/rules/rules.go
  14. 251
      operator/internal/handlers/internal/rules/rules_test.go
  15. 33
      operator/internal/handlers/internal/storage/ca_configmap.go
  16. 8
      operator/internal/handlers/internal/storage/ca_configmap_test.go
  17. 34
      operator/internal/handlers/internal/storage/secrets.go
  18. 10
      operator/internal/handlers/internal/storage/secrets_test.go
  19. 91
      operator/internal/handlers/internal/storage/storage.go
  20. 477
      operator/internal/handlers/internal/storage/storage_test.go
  21. 231
      operator/internal/handlers/lokistack_create_or_update.go
  22. 914
      operator/internal/handlers/lokistack_create_or_update_test.go

@ -6,7 +6,6 @@ import (
"github.com/ViaQ/logerr/v2/kverrors"
configv1 "github.com/openshift/api/config/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
@ -14,11 +13,11 @@ import (
"github.com/grafana/loki/operator/internal/status"
)
// GetOpenShiftBaseDomain returns the cluster DNS base domain on OpenShift
// getOpenShiftBaseDomain returns the cluster DNS base domain on OpenShift
// clusters to auto-create redirect URLs for OpenShift Auth or an error.
// If the config.openshift.io/DNS object is not found the whole lokistack
// resoure is set to a degraded state.
func GetOpenShiftBaseDomain(ctx context.Context, k k8s.Client, req ctrl.Request) (string, error) {
func getOpenShiftBaseDomain(ctx context.Context, k k8s.Client) (string, error) {
var cluster configv1.DNS
key := client.ObjectKey{Name: "cluster"}
if err := k.Get(ctx, key, &cluster); err != nil {

@ -0,0 +1,87 @@
package gateway
import (
"context"
"fmt"
"github.com/go-logr/logr"
configv1 "github.com/grafana/loki/operator/apis/config/v1"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/handlers/internal/openshift"
"github.com/grafana/loki/operator/internal/manifests"
"github.com/grafana/loki/operator/internal/status"
)
// BuildOptions returns the options needed to generate Kubernetes resource
// manifests for the lokistack-gateway.
// The returned error can be a status.DegradedError in the following cases:
// - The tenants spec is missing.
// - The tenants spec is invalid.
func BuildOptions(ctx context.Context, log logr.Logger, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (string, manifests.Tenants, error) {
var (
err error
baseDomain string
secrets []*manifests.TenantSecrets
configs map[string]manifests.TenantConfig
tenants manifests.Tenants
)
if !fg.LokiStackGateway {
return "", tenants, nil
}
if stack.Spec.Tenants == nil {
return "", tenants, &status.DegradedError{
Message: "Invalid tenants configuration: TenantsSpec cannot be nil when gateway flag is enabled",
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
}
if err = validateModes(stack); err != nil {
return "", tenants, &status.DegradedError{
Message: fmt.Sprintf("Invalid tenants configuration: %s", err),
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
}
switch stack.Spec.Tenants.Mode {
case lokiv1.OpenshiftLogging, lokiv1.OpenshiftNetwork:
baseDomain, err = getOpenShiftBaseDomain(ctx, k)
if err != nil {
return "", tenants, err
}
if stack.Spec.Proxy == nil {
// If the LokiStack has no proxy set but there is a cluster-wide proxy setting,
// set the LokiStack proxy to that.
ocpProxy, proxyErr := openshift.GetProxy(ctx, k)
if proxyErr != nil {
return "", tenants, proxyErr
}
stack.Spec.Proxy = ocpProxy
}
default:
secrets, err = getTenantSecrets(ctx, k, stack)
if err != nil {
return "", tenants, err
}
}
// extract the existing tenant's id, cookieSecret if exists, otherwise create new.
configs, err = getTenantConfigFromSecret(ctx, k, stack)
if err != nil {
log.Error(err, "error in getting tenant secret data")
}
tenants = manifests.Tenants{
Secrets: secrets,
Configs: configs,
}
return baseDomain, tenants, nil
}

@ -0,0 +1,390 @@
package gateway
import (
"context"
"io"
"testing"
"github.com/ViaQ/logerr/v2/log"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
configv1 "github.com/grafana/loki/operator/apis/config/v1"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
"github.com/grafana/loki/operator/internal/status"
)
var (
logger = log.NewLogger("testing", log.WithOutput(io.Discard))
defaultSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{
"endpoint": []byte("s3://your-endpoint"),
"region": []byte("a-region"),
"bucketnames": []byte("bucket1,bucket2"),
"access_key_id": []byte("a-secret-id"),
"access_key_secret": []byte("a-secret-key"),
},
}
defaultGatewaySecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-gateway-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{
"clientID": []byte("client-123"),
"clientSecret": []byte("client-secret-xyz"),
"issuerCAPath": []byte("/tmp/test/ca.pem"),
},
}
invalidSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{},
}
)
func TestBuildOptions_WhenInvalidTenantsConfiguration_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid tenants configuration: mandatory configuration - missing OPA Url",
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
fg := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: nil,
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, _, err := BuildOptions(context.TODO(), logger, k, stack, fg)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenMissingGatewaySecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing secrets for tenant test",
Reason: lokiv1.ReasonMissingGatewayTenantSecret,
Requeue: true,
}
fg := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
o, ok := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && ok {
k.SetClientObject(o, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, _, err := BuildOptions(context.TODO(), logger, k, stack, fg)
// make sure error is returned to re-trigger reconciliation
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenInvalidGatewaySecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid gateway tenant secret contents",
Reason: lokiv1.ReasonInvalidGatewayTenantSecret,
Requeue: true,
}
fg := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: invalidSecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
o, ok := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && ok {
k.SetClientObject(o, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
if name.Name == invalidSecret.Name {
k.SetClientObject(object, &invalidSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, _, err := BuildOptions(context.TODO(), logger, k, stack, fg)
// make sure error is returned to re-trigger reconciliation
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_MissingTenantsSpec_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid tenants configuration: TenantsSpec cannot be nil when gateway flag is enabled",
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
fg := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: nil,
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
o, ok := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && ok {
k.SetClientObject(o, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, _, err := BuildOptions(context.TODO(), logger, k, stack, fg)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}

@ -6,8 +6,7 @@ import (
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
)
// ValidateModes validates the tenants mode specification.
func ValidateModes(stack lokiv1.LokiStack) error {
func validateModes(stack *lokiv1.LokiStack) error {
if stack.Spec.Tenants.Mode == lokiv1.Static {
if stack.Spec.Tenants.Authentication == nil {
return kverrors.New("mandatory configuration - missing tenants' authentication configuration")

@ -13,13 +13,13 @@ func TestValidateModes_StaticMode(t *testing.T) {
type test struct {
name string
wantErr string
stack lokiv1.LokiStack
stack *lokiv1.LokiStack
}
table := []test{
{
name: "missing authentication spec",
wantErr: "mandatory configuration - missing tenants' authentication configuration",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -39,7 +39,7 @@ func TestValidateModes_StaticMode(t *testing.T) {
{
name: "missing roles spec",
wantErr: "mandatory configuration - missing roles configuration",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -74,7 +74,7 @@ func TestValidateModes_StaticMode(t *testing.T) {
{
name: "missing role bindings spec",
wantErr: "mandatory configuration - missing role bindings configuration",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -117,7 +117,7 @@ func TestValidateModes_StaticMode(t *testing.T) {
{
name: "incompatible OPA URL provided",
wantErr: "incompatible configuration - OPA URL not required for mode static",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -174,7 +174,7 @@ func TestValidateModes_StaticMode(t *testing.T) {
{
name: "all set",
wantErr: "",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -231,7 +231,7 @@ func TestValidateModes_StaticMode(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
err := ValidateModes(tst.stack)
err := validateModes(tst.stack)
if tst.wantErr != "" {
require.EqualError(t, err, tst.wantErr)
}
@ -243,13 +243,13 @@ func TestValidateModes_DynamicMode(t *testing.T) {
type test struct {
name string
wantErr string
stack lokiv1.LokiStack
stack *lokiv1.LokiStack
}
table := []test{
{
name: "missing authentication spec",
wantErr: "mandatory configuration - missing tenants configuration",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -269,7 +269,7 @@ func TestValidateModes_DynamicMode(t *testing.T) {
{
name: "missing OPA URL spec",
wantErr: "mandatory configuration - missing OPA Url",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -304,7 +304,7 @@ func TestValidateModes_DynamicMode(t *testing.T) {
{
name: "incompatible roles configuration provided",
wantErr: "incompatible configuration - static roles not required for mode dynamic",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -349,7 +349,7 @@ func TestValidateModes_DynamicMode(t *testing.T) {
{
name: "incompatible roleBindings configuration provided",
wantErr: "incompatible configuration - static roleBindings not required for mode dynamic",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -398,7 +398,7 @@ func TestValidateModes_DynamicMode(t *testing.T) {
{
name: "all set",
wantErr: "",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -438,7 +438,7 @@ func TestValidateModes_DynamicMode(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
err := ValidateModes(tst.stack)
err := validateModes(tst.stack)
if tst.wantErr != "" {
require.EqualError(t, err, tst.wantErr)
}
@ -450,13 +450,13 @@ func TestValidateModes_OpenshiftLoggingMode(t *testing.T) {
type test struct {
name string
wantErr string
stack lokiv1.LokiStack
stack *lokiv1.LokiStack
}
table := []test{
{
name: "incompatible authentication spec provided",
wantErr: "incompatible configuration - custom tenants configuration not required",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -488,7 +488,7 @@ func TestValidateModes_OpenshiftLoggingMode(t *testing.T) {
{
name: "incompatible authorization spec provided",
wantErr: "incompatible configuration - custom tenants configuration not required",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -514,7 +514,7 @@ func TestValidateModes_OpenshiftLoggingMode(t *testing.T) {
{
name: "all set",
wantErr: "",
stack: lokiv1.LokiStack{
stack: &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
@ -537,7 +537,7 @@ func TestValidateModes_OpenshiftLoggingMode(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
err := ValidateModes(tst.stack)
err := validateModes(tst.stack)
if tst.wantErr != "" {
require.EqualError(t, err, tst.wantErr)
}

@ -6,10 +6,10 @@ import (
"github.com/ViaQ/logerr/v2/kverrors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/json"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/manifests"
)
@ -35,11 +35,11 @@ type openShiftSpec struct {
CookieSecret string `json:"cookieSecret"`
}
// GetTenantConfigSecretData returns the tenantName, tenantId, cookieSecret
// getTenantConfigFromSecret returns the tenantName, tenantId, cookieSecret
// clusters to auto-create redirect URLs for OpenShift Auth or an error.
func GetTenantConfigSecretData(ctx context.Context, k k8s.Client, req ctrl.Request) (map[string]manifests.TenantConfig, error) {
func getTenantConfigFromSecret(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack) (map[string]manifests.TenantConfig, error) {
var tenantSecret corev1.Secret
key := client.ObjectKey{Name: manifests.GatewayName(req.Name), Namespace: req.Namespace}
key := client.ObjectKey{Name: manifests.GatewayName(stack.Name), Namespace: stack.Namespace}
if err := k.Get(ctx, key, &tenantSecret); err != nil {
return nil, kverrors.Wrap(err, "couldn't find tenant secret.")
}

@ -10,9 +10,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
"github.com/grafana/loki/operator/internal/manifests"
)
@ -38,8 +38,8 @@ tenants:
func TestGetTenantConfigSecretData_SecretExist(t *testing.T) {
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
s := &lokiv1.LokiStack{
ObjectMeta: metav1.ObjectMeta{
Name: "lokistack-dev",
Namespace: "some-ns",
},
@ -60,7 +60,7 @@ func TestGetTenantConfigSecretData_SecretExist(t *testing.T) {
return nil
}
ts, err := GetTenantConfigSecretData(context.TODO(), k, r)
ts, err := getTenantConfigFromSecret(context.TODO(), k, s)
require.NotNil(t, ts)
require.NoError(t, err)
@ -86,8 +86,8 @@ func TestGetTenantConfigSecretData_SecretExist(t *testing.T) {
func TestGetTenantConfigSecretData_SecretNotExist(t *testing.T) {
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
s := &lokiv1.LokiStack{
ObjectMeta: metav1.ObjectMeta{
Name: "lokistack-dev",
Namespace: "some-ns",
},
@ -97,7 +97,7 @@ func TestGetTenantConfigSecretData_SecretNotExist(t *testing.T) {
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
ts, err := GetTenantConfigSecretData(context.TODO(), k, r)
ts, err := getTenantConfigFromSecret(context.TODO(), k, s)
require.Nil(t, ts)
require.Error(t, err)
}

@ -7,7 +7,6 @@ import (
"github.com/ViaQ/logerr/v2/kverrors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
@ -16,14 +15,13 @@ import (
"github.com/grafana/loki/operator/internal/status"
)
// GetTenantSecrets returns the list to gateway tenant secrets for a tenant mode.
// getTenantSecrets returns the list to gateway tenant secrets for a tenant mode.
// For modes static and dynamic the secrets are fetched from external provided
// secrets. For modes openshift-logging and openshift-network a secret per default tenants are created.
// All secrets live in the same namespace as the lokistack request.
func GetTenantSecrets(
func getTenantSecrets(
ctx context.Context,
k k8s.Client,
req ctrl.Request,
stack *lokiv1.LokiStack,
) ([]*manifests.TenantSecrets, error) {
var (
@ -34,7 +32,7 @@ func GetTenantSecrets(
for _, tenant := range stack.Spec.Tenants.Authentication {
switch {
case tenant.OIDC != nil:
key := client.ObjectKey{Name: tenant.OIDC.Secret.Name, Namespace: req.Namespace}
key := client.ObjectKey{Name: tenant.OIDC.Secret.Name, Namespace: stack.Namespace}
if err := k.Get(ctx, key, &gatewaySecret); err != nil {
if apierrors.IsNotFound(err) {
return nil, &status.DegradedError{
@ -60,7 +58,7 @@ func GetTenantSecrets(
OIDCSecret: oidcSecret,
}
if tenant.OIDC.IssuerCA != nil {
caPath, err := extractCAPath(ctx, k, req.Namespace, tenant.TenantName, tenant.OIDC.IssuerCA)
caPath, err := extractCAPath(ctx, k, stack.Namespace, tenant.TenantName, tenant.OIDC.IssuerCA)
if err != nil {
return nil, err
}
@ -68,7 +66,7 @@ func GetTenantSecrets(
}
tenantSecrets = append(tenantSecrets, tennantSecret)
case tenant.MTLS != nil:
caPath, err := extractCAPath(ctx, k, req.Namespace, tenant.TenantName, tenant.MTLS.CA)
caPath, err := extractCAPath(ctx, k, stack.Namespace, tenant.TenantName, tenant.MTLS.CA)
if err != nil {
return nil, err
}

@ -9,7 +9,6 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
@ -93,13 +92,6 @@ func TestGetTenantSecrets(t *testing.T) {
} {
t.Run(strings.Join([]string{string(mode), tc.name}, "_"), func(t *testing.T) {
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
s := &lokiv1.LokiStack{
ObjectMeta: metav1.ObjectMeta{
Name: "mystack",
@ -119,7 +111,7 @@ func TestGetTenantSecrets(t *testing.T) {
}
return nil
}
ts, err := GetTenantSecrets(context.TODO(), k, r, s)
ts, err := getTenantSecrets(context.TODO(), k, s)
require.NoError(t, err)
require.ElementsMatch(t, ts, tc.expected)
})

@ -4,25 +4,49 @@ import (
"context"
"github.com/ViaQ/logerr/v2/kverrors"
"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/manifests"
)
// RemoveRulesConfigMap removes the rules configmaps if any exists.
func RemoveRulesConfigMap(ctx context.Context, req ctrl.Request, c client.Client) error {
// Cleanup removes the ruler component's statefulset and configmaps if available, or
// else it returns an error to retry the reconciliation loop.
func Cleanup(ctx context.Context, log logr.Logger, k k8s.Client, stack *v1.LokiStack) error {
if stack.Spec.Rules != nil && stack.Spec.Rules.Enabled {
return nil
}
stackKey := client.ObjectKeyFromObject(stack)
// Clean up ruler resources
if err := removeRulesConfigMap(ctx, k, stackKey); err != nil {
log.Error(err, "failed to remove rules ConfigMap")
return err
}
if err := removeRuler(ctx, k, stackKey); err != nil {
log.Error(err, "failed to remove ruler StatefulSet")
return err
}
return nil
}
func removeRulesConfigMap(ctx context.Context, c client.Client, key client.ObjectKey) error {
var rulesCmList corev1.ConfigMapList
err := c.List(ctx, &rulesCmList, &client.ListOptions{
Namespace: req.Namespace,
Namespace: key.Namespace,
LabelSelector: labels.SelectorFromSet(labels.Set{
"app.kubernetes.io/component": manifests.LabelRulerComponent,
"app.kubernetes.io/instance": req.Name,
"app.kubernetes.io/instance": key.Name,
}),
})
if err != nil {
@ -41,10 +65,9 @@ func RemoveRulesConfigMap(ctx context.Context, req ctrl.Request, c client.Client
return nil
}
// RemoveRuler removes the ruler statefulset if it exists.
func RemoveRuler(ctx context.Context, req ctrl.Request, c client.Client) error {
func removeRuler(ctx context.Context, c client.Client, stack client.ObjectKey) error {
// Check if the Statefulset exists before proceeding.
key := client.ObjectKey{Name: manifests.RulerName(req.Name), Namespace: req.Namespace}
key := client.ObjectKey{Name: manifests.RulerName(stack.Name), Namespace: stack.Namespace}
var ruler appsv1.StatefulSet
if err := c.Get(ctx, key, &ruler); err != nil {

@ -0,0 +1,223 @@
package rules
import (
"context"
"io"
"testing"
"github.com/ViaQ/logerr/v2/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
)
var (
logger = log.NewLogger("testing", log.WithOutput(io.Discard))
defaultSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{
"endpoint": []byte("s3://your-endpoint"),
"region": []byte("a-region"),
"bucketnames": []byte("bucket1,bucket2"),
"access_key_id": []byte("a-secret-id"),
"access_key_secret": []byte("a-secret-key"),
},
}
defaultGatewaySecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-gateway-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{
"clientID": []byte("client-123"),
"clientSecret": []byte("client-secret-xyz"),
"issuerCAPath": []byte("/tmp/test/ca.pem"),
},
}
rulesCM = corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack-rules-0",
Namespace: "some-ns",
},
}
rulerSS = appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack-ruler",
Namespace: "some-ns",
},
}
)
func TestCleanup_RemovesRulerResourcesWhenDisabled(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
stack := lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Rules: &lokiv1.RulesSpec{
Enabled: true,
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, out client.Object, _ ...client.GetOption) error {
_, ok := out.(*lokiv1.RulerConfig)
if ok {
return apierrors.NewNotFound(schema.GroupResource{}, "no ruler config")
}
_, isLokiStack := out.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(out, &stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(out, &defaultSecret)
return nil
}
if defaultGatewaySecret.Name == name.Name {
k.SetClientObject(out, &defaultGatewaySecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error {
assert.Equal(t, r.Namespace, o.GetNamespace())
return nil
}
k.StatusStub = func() client.StatusWriter { return sw }
k.DeleteStub = func(_ context.Context, o client.Object, _ ...client.DeleteOption) error {
assert.Equal(t, r.Namespace, o.GetNamespace())
return nil
}
k.ListStub = func(_ context.Context, list client.ObjectList, options ...client.ListOption) error {
switch list.(type) {
case *corev1.ConfigMapList:
k.SetClientObjectList(list, &corev1.ConfigMapList{
Items: []corev1.ConfigMap{
rulesCM,
},
})
}
return nil
}
err := Cleanup(context.TODO(), logger, k, &stack)
require.NoError(t, err)
// make sure delete not called
require.Zero(t, k.DeleteCallCount())
// Disable the ruler
stack.Spec.Rules.Enabled = false
// Get should return ruler resources
k.GetStub = func(_ context.Context, name types.NamespacedName, out client.Object, _ ...client.GetOption) error {
_, ok := out.(*lokiv1.RulerConfig)
if ok {
return apierrors.NewNotFound(schema.GroupResource{}, "no ruler config")
}
if rulesCM.Name == name.Name {
k.SetClientObject(out, &rulesCM)
return nil
}
if rulerSS.Name == name.Name {
k.SetClientObject(out, &rulerSS)
return nil
}
_, isLokiStack := out.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(out, &stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(out, &defaultSecret)
return nil
}
if defaultGatewaySecret.Name == name.Name {
k.SetClientObject(out, &defaultGatewaySecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
err = Cleanup(context.TODO(), logger, k, &stack)
require.NoError(t, err)
// make sure delete was called twice (delete rules configmap and ruler statefulset)
require.Equal(t, 2, k.DeleteCallCount())
}

@ -5,19 +5,16 @@ import (
"github.com/ViaQ/logerr/v2/kverrors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
)
// GetRulerConfig returns the ruler config spec for a lokistack resource or an error.
// getRulerConfig returns the ruler config spec for a lokistack resource or an error.
// If the config is not found, we skip without an error.
func GetRulerConfig(ctx context.Context, k k8s.Client, req ctrl.Request) (*lokiv1.RulerConfigSpec, error) {
func getRulerConfig(ctx context.Context, k k8s.Client, key client.ObjectKey) (*lokiv1.RulerConfigSpec, error) {
var rc lokiv1.RulerConfig
key := client.ObjectKey{Name: req.Name, Namespace: req.Namespace}
if err := k.Get(ctx, key, &rc); err != nil {
if apierrors.IsNotFound(err) {
return nil, nil

@ -4,20 +4,114 @@ import (
"context"
"github.com/ViaQ/logerr/v2/kverrors"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/handlers/internal/openshift"
"github.com/grafana/loki/operator/internal/manifests"
manifestsocp "github.com/grafana/loki/operator/internal/manifests/openshift"
"github.com/grafana/loki/operator/internal/status"
)
// List returns a slice of AlertingRules and a slice of RecordingRules for the given spec or an error. Three cases apply:
// - Return only matching rules in the stack namespace if no namespace selector given.
// - Return only matching rules in the stack namespace and in namespaces matching the namespace selector.
// - Return no rules if rules selector does not apply at all.
func List(ctx context.Context, k k8s.Client, stackNs string, rs *lokiv1.RulesSpec) ([]lokiv1.AlertingRule, []lokiv1.RecordingRule, error) {
// BuildOptions returns the ruler options needed to generate Kubernetes resource manifests.
// The returned error can be a status.DegradedError in the following cases:
// - When remote write is enabled and the authorization Secret is missing.
// - When remote write is enabled and the authorization Secret data is invalid.
func BuildOptions(
ctx context.Context,
log logr.Logger,
k k8s.Client,
stack *lokiv1.LokiStack,
) ([]lokiv1.AlertingRule, []lokiv1.RecordingRule, manifests.Ruler, manifestsocp.Options, error) {
if stack.Spec.Rules == nil || !stack.Spec.Rules.Enabled {
return nil, nil, manifests.Ruler{}, manifestsocp.Options{}, nil
}
var (
err error
alertingRules []lokiv1.AlertingRule
recordingRules []lokiv1.RecordingRule
rulerConfig *lokiv1.RulerConfigSpec
rulerSecret *manifests.RulerSecret
ruler manifests.Ruler
ocpOpts manifestsocp.Options
stackKey = client.ObjectKeyFromObject(stack)
)
alertingRules, recordingRules, err = listRules(ctx, k, stack.Namespace, stack.Spec.Rules)
if err != nil {
log.Error(err, "failed to lookup rules", "spec", stack.Spec.Rules)
}
rulerConfig, err = getRulerConfig(ctx, k, stackKey)
if err != nil {
log.Error(err, "failed to lookup ruler config", "key", stackKey)
}
if rulerConfig != nil && rulerConfig.RemoteWriteSpec != nil && rulerConfig.RemoteWriteSpec.ClientSpec != nil {
var rs corev1.Secret
key := client.ObjectKey{Name: rulerConfig.RemoteWriteSpec.ClientSpec.AuthorizationSecretName, Namespace: stack.Namespace}
if err = k.Get(ctx, key, &rs); err != nil {
if apierrors.IsNotFound(err) {
return nil, nil, ruler, ocpOpts, &status.DegradedError{
Message: "Missing ruler remote write authorization secret",
Reason: lokiv1.ReasonMissingRulerSecret,
Requeue: false,
}
}
return nil, nil, ruler, ocpOpts, kverrors.Wrap(err, "failed to lookup lokistack ruler secret", "name", key)
}
rulerSecret, err = ExtractRulerSecret(&rs, rulerConfig.RemoteWriteSpec.ClientSpec.AuthorizationType)
if err != nil {
return nil, nil, ruler, ocpOpts, &status.DegradedError{
Message: "Invalid ruler remote write authorization secret contents",
Reason: lokiv1.ReasonInvalidRulerSecret,
Requeue: false,
}
}
}
ocpAmEnabled, err := openshift.AlertManagerSVCExists(ctx, stack.Spec, k)
if err != nil {
log.Error(err, "failed to check OCP AlertManager")
return nil, nil, ruler, ocpOpts, err
}
ocpUWAmEnabled, err := openshift.UserWorkloadAlertManagerSVCExists(ctx, stack.Spec, k)
if err != nil {
log.Error(err, "failed to check OCP User Workload AlertManager")
return nil, nil, ruler, ocpOpts, err
}
ruler = manifests.Ruler{
Spec: rulerConfig,
Secret: rulerSecret,
}
ocpOpts = manifestsocp.Options{
BuildOpts: manifestsocp.BuildOptions{
AlertManagerEnabled: ocpAmEnabled,
UserWorkloadAlertManagerEnabled: ocpUWAmEnabled,
},
}
return alertingRules, recordingRules, ruler, ocpOpts, nil
}
// listRules returns a slice of AlertingRules and a slice of RecordingRules for the given spec or an error.
// Three cases apply:
// - Return only matching rules in the stack namespace if no namespace selector is given.
// - Return only matching rules in the stack namespace and in namespaces matching the namespace selector.
// - Return no rules if rules selector does not apply at all.
func listRules(ctx context.Context, k k8s.Client, stackNs string, rs *lokiv1.RulesSpec) ([]lokiv1.AlertingRule, []lokiv1.RecordingRule, error) {
nsl, err := selectRulesNamespaces(ctx, k, stackNs, rs)
if err != nil {
return nil, nil, err

@ -1,4 +1,4 @@
package rules_test
package rules
import (
"context"
@ -11,13 +11,252 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
"github.com/grafana/loki/operator/internal/handlers/internal/rules"
"github.com/grafana/loki/operator/internal/status"
)
func TestBuildOptions_WhenMissingRemoteWriteSecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
stack := lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Rules: &lokiv1.RulesSpec{
Enabled: true,
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
rulerCfg := &lokiv1.RulerConfig{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.RulerConfigSpec{
RemoteWriteSpec: &lokiv1.RemoteWriteSpec{
Enabled: true,
ClientSpec: &lokiv1.RemoteWriteClientSpec{
AuthorizationType: lokiv1.BasicAuthorization,
AuthorizationSecretName: "test",
},
},
},
}
degradedErr := &status.DegradedError{
Message: "Missing ruler remote write authorization secret",
Reason: lokiv1.ReasonMissingRulerSecret,
Requeue: false,
}
k.GetStub = func(_ context.Context, name types.NamespacedName, out client.Object, _ ...client.GetOption) error {
_, isRulerConfig := out.(*lokiv1.RulerConfig)
if r.Name == name.Name && r.Namespace == name.Namespace && isRulerConfig {
k.SetClientObject(out, rulerCfg)
return nil
}
_, isLokiStack := out.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(out, &stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(out, &defaultSecret)
return nil
}
if defaultGatewaySecret.Name == name.Name {
k.SetClientObject(out, &defaultGatewaySecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, _, _, _, err := BuildOptions(context.TODO(), logger, k, &stack)
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenInvalidRemoteWriteSecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
stack := lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Rules: &lokiv1.RulesSpec{
Enabled: true,
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
rulerCfg := &lokiv1.RulerConfig{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.RulerConfigSpec{
RemoteWriteSpec: &lokiv1.RemoteWriteSpec{
Enabled: true,
ClientSpec: &lokiv1.RemoteWriteClientSpec{
AuthorizationType: lokiv1.BasicAuthorization,
AuthorizationSecretName: "some-client-secret",
},
},
},
}
invalidSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-client-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{},
}
degradedErr := &status.DegradedError{
Message: "Invalid ruler remote write authorization secret contents",
Reason: lokiv1.ReasonInvalidRulerSecret,
Requeue: false,
}
k.GetStub = func(_ context.Context, name types.NamespacedName, out client.Object, _ ...client.GetOption) error {
_, isRulerConfig := out.(*lokiv1.RulerConfig)
if r.Name == name.Name && r.Namespace == name.Namespace && isRulerConfig {
k.SetClientObject(out, rulerCfg)
return nil
}
_, isLokiStack := out.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(out, &stack)
return nil
}
if invalidSecret.Name == name.Name {
k.SetClientObject(out, &invalidSecret)
return nil
}
if defaultGatewaySecret.Name == name.Name {
k.SetClientObject(out, &defaultGatewaySecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, _, _, _, err := BuildOptions(context.TODO(), logger, k, &stack)
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestList_AlertingRulesMatchSelector_WithDefaultStackNamespaceRules(t *testing.T) {
const stackNs = "some-ns"
@ -83,7 +322,7 @@ func TestList_AlertingRulesMatchSelector_WithDefaultStackNamespaceRules(t *testi
return nil
}
rules, _, err := rules.List(context.TODO(), k, stackNs, rs)
rules, _, err := listRules(context.TODO(), k, stackNs, rs)
require.NoError(t, err)
require.NotEmpty(t, rules)
@ -185,7 +424,7 @@ func TestList_AlertingRulesMatchSelector_FilteredByNamespaceSelector(t *testing.
return nil
}
rules, _, err := rules.List(context.TODO(), k, stackNs, rs)
rules, _, err := listRules(context.TODO(), k, stackNs, rs)
require.NoError(t, err)
require.NotEmpty(t, rules)
@ -257,7 +496,7 @@ func TestList_RecordingRulesMatchSelector_WithDefaultStackNamespaceRules(t *test
return nil
}
_, rules, err := rules.List(context.TODO(), k, stackNs, rs)
_, rules, err := listRules(context.TODO(), k, stackNs, rs)
require.NoError(t, err)
require.NotEmpty(t, rules)
@ -358,7 +597,7 @@ func TestList_RecordingRulesMatchSelector_FilteredByNamespaceSelector(t *testing
return nil
}
_, rules, err := rules.List(context.TODO(), k, stackNs, rs)
_, rules, err := listRules(context.TODO(), k, stackNs, rs)
require.NoError(t, err)
require.NotEmpty(t, rules)

@ -1,10 +1,22 @@
package storage
import (
"context"
"crypto/sha1"
"fmt"
"github.com/ViaQ/logerr/v2/kverrors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/status"
)
const (
defaultCAKey = "service-ca.crt"
)
type caKeyError string
@ -13,9 +25,26 @@ func (e caKeyError) Error() string {
return fmt.Sprintf("key not present or data empty: %s", string(e))
}
// CheckCAConfigMap checks if the given CA configMap has an non-empty entry for the key used as CA certificate.
func getCAConfigMap(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, name string) (*corev1.ConfigMap, error) {
var cm corev1.ConfigMap
key := client.ObjectKey{Name: name, Namespace: stack.Namespace}
if err := k.Get(ctx, key, &cm); err != nil {
if apierrors.IsNotFound(err) {
return nil, &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
}
return nil, kverrors.Wrap(err, "failed to lookup lokistack object storage CA config map", "name", key)
}
return &cm, nil
}
// checkCAConfigMap checks if the given CA configMap has an non-empty entry for the key used as CA certificate.
// If the key is present it will return a hash of the current key name and contents.
func CheckCAConfigMap(cm *corev1.ConfigMap, key string) (string, error) {
func checkCAConfigMap(cm *corev1.ConfigMap, key string) (string, error) {
data := cm.Data[key]
if data == "" {
return "", caKeyError(key)

@ -1,15 +1,13 @@
package storage_test
package storage
import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"github.com/grafana/loki/operator/internal/handlers/internal/storage"
)
func TestIsValidConfigMap(t *testing.T) {
func TestCheckValidConfigMap(t *testing.T) {
type test struct {
name string
cm *corev1.ConfigMap
@ -47,7 +45,7 @@ func TestIsValidConfigMap(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
hash, err := storage.CheckCAConfigMap(tst.cm, "service-ca.crt")
hash, err := checkCAConfigMap(tst.cm, "service-ca.crt")
require.Equal(t, tst.wantHash, hash)
if tst.wantErrorMsg == "" {

@ -1,24 +1,46 @@
package storage
import (
"context"
"crypto/sha1"
"fmt"
"sort"
"github.com/ViaQ/logerr/v2/kverrors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/manifests/storage"
"github.com/grafana/loki/operator/internal/status"
)
var hashSeparator = []byte(",")
// ExtractSecret reads a k8s secret into a manifest object storage struct if valid.
func ExtractSecret(s *corev1.Secret, secretType lokiv1.ObjectStorageSecretType) (*storage.Options, error) {
func getSecret(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack) (*corev1.Secret, error) {
var storageSecret corev1.Secret
key := client.ObjectKey{Name: stack.Spec.Storage.Secret.Name, Namespace: stack.Namespace}
if err := k.Get(ctx, key, &storageSecret); err != nil {
if apierrors.IsNotFound(err) {
return nil, &status.DegradedError{
Message: "Missing object storage secret",
Reason: lokiv1.ReasonMissingObjectStorageSecret,
Requeue: false,
}
}
return nil, kverrors.Wrap(err, "failed to lookup lokistack storage secret", "name", key)
}
return &storageSecret, nil
}
// extractSecret reads a k8s secret into a manifest object storage struct if valid.
func extractSecret(s *corev1.Secret, secretType lokiv1.ObjectStorageSecretType) (storage.Options, error) {
hash, err := hashSecretData(s)
if err != nil {
return nil, kverrors.Wrap(err, "error calculating hash for secret", "type", secretType)
return storage.Options{}, kverrors.Wrap(err, "error calculating hash for secret", "type", secretType)
}
storageOpts := storage.Options{
@ -39,13 +61,13 @@ func ExtractSecret(s *corev1.Secret, secretType lokiv1.ObjectStorageSecretType)
case lokiv1.ObjectStorageSecretAlibabaCloud:
storageOpts.AlibabaCloud, err = extractAlibabaCloudConfigSecret(s)
default:
return nil, kverrors.New("unknown secret type", "type", secretType)
return storage.Options{}, kverrors.New("unknown secret type", "type", secretType)
}
if err != nil {
return nil, err
return storage.Options{}, err
}
return &storageOpts, nil
return storageOpts, nil
}
func hashSecretData(s *corev1.Secret) (string, error) {

@ -135,7 +135,7 @@ func TestAzureExtract(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
opts, err := ExtractSecret(tst.secret, lokiv1.ObjectStorageSecretAzure)
opts, err := extractSecret(tst.secret, lokiv1.ObjectStorageSecretAzure)
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
@ -186,7 +186,7 @@ func TestGCSExtract(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
_, err := ExtractSecret(tst.secret, lokiv1.ObjectStorageSecretGCS)
_, err := extractSecret(tst.secret, lokiv1.ObjectStorageSecretGCS)
if !tst.wantErr {
require.NoError(t, err)
}
@ -360,7 +360,7 @@ func TestS3Extract(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
opts, err := ExtractSecret(tst.secret, lokiv1.ObjectStorageSecretS3)
opts, err := extractSecret(tst.secret, lokiv1.ObjectStorageSecretS3)
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
@ -509,7 +509,7 @@ func TestSwiftExtract(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
opts, err := ExtractSecret(tst.secret, lokiv1.ObjectStorageSecretSwift)
opts, err := extractSecret(tst.secret, lokiv1.ObjectStorageSecretSwift)
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
@ -583,7 +583,7 @@ func TestAlibabaCloudExtract(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
opts, err := ExtractSecret(tst.secret, lokiv1.ObjectStorageSecretAlibabaCloud)
opts, err := extractSecret(tst.secret, lokiv1.ObjectStorageSecretAlibabaCloud)
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)

@ -0,0 +1,91 @@
package storage
import (
"context"
"fmt"
"time"
configv1 "github.com/grafana/loki/operator/apis/config/v1"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/manifests/storage"
"github.com/grafana/loki/operator/internal/status"
)
// BuildOptions returns the object storage options to generate Kubernetes resource manifests
// which require access to object storage buckets.
// The returned error can be a status.DegradedError in the following cases:
// - The user-provided object storage secret is missing.
// - The object storage Secret data is invalid.
// - The object storage schema config is invalid.
// - The object storage CA ConfigMap is missing if one referenced.
// - The object storage CA ConfigMap data is invalid.
func BuildOptions(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (storage.Options, error) {
storageSecret, err := getSecret(ctx, k, stack)
if err != nil {
return storage.Options{}, err
}
objStore, err := extractSecret(storageSecret, stack.Spec.Storage.Secret.Type)
if err != nil {
return storage.Options{}, &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage secret contents: %s", err),
Reason: lokiv1.ReasonInvalidObjectStorageSecret,
Requeue: false,
}
}
objStore.OpenShiftEnabled = fg.OpenShift.Enabled
storageSchemas, err := storage.BuildSchemaConfig(
time.Now().UTC(),
stack.Spec.Storage,
stack.Status.Storage,
)
if err != nil {
return storage.Options{}, &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage schema contents: %s", err),
Reason: lokiv1.ReasonInvalidObjectStorageSchema,
Requeue: false,
}
}
objStore.Schemas = storageSchemas
if stack.Spec.Storage.TLS == nil {
return objStore, nil
}
tlsConfig := stack.Spec.Storage.TLS
if tlsConfig.CA == "" {
return storage.Options{}, &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
}
cm, err := getCAConfigMap(ctx, k, stack, tlsConfig.CA)
if err != nil {
return storage.Options{}, err
}
caKey := defaultCAKey
if tlsConfig.CAKey != "" {
caKey = tlsConfig.CAKey
}
var caHash string
caHash, err = checkCAConfigMap(cm, caKey)
if err != nil {
return storage.Options{}, &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage CA configmap contents: %s", err),
Reason: lokiv1.ReasonInvalidObjectStorageCAConfigMap,
Requeue: false,
}
}
objStore.SecretSHA1 = fmt.Sprintf("%s;%s", objStore.SecretSHA1, caHash)
objStore.TLS = &storage.TLSConfig{CA: cm.Name, Key: caKey}
return objStore, nil
}

@ -0,0 +1,477 @@
package storage
import (
"context"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
configv1 "github.com/grafana/loki/operator/apis/config/v1"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
"github.com/grafana/loki/operator/internal/status"
)
var (
featureGates = configv1.FeatureGates{
ServiceMonitors: false,
ServiceMonitorTLSEndpoints: false,
BuiltInCertManagement: configv1.BuiltInCertManagement{
Enabled: true,
CACertValidity: "10m",
CACertRefresh: "5m",
CertValidity: "2m",
CertRefresh: "1m",
},
}
defaultSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{
"endpoint": []byte("s3://your-endpoint"),
"region": []byte("a-region"),
"bucketnames": []byte("bucket1,bucket2"),
"access_key_id": []byte("a-secret-id"),
"access_key_secret": []byte("a-secret-key"),
},
}
invalidSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{},
}
invalidCAConfigMap = corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-ca-configmap",
Namespace: "some-ns",
},
Data: map[string]string{},
}
)
func TestBuildOptions_WhenMissingSecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing object storage secret",
Reason: lokiv1.ReasonMissingObjectStorageSecret,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, err := BuildOptions(context.TODO(), k, stack, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenInvalidSecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid object storage secret contents: missing secret field",
Reason: lokiv1.ReasonInvalidObjectStorageSecret,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: invalidSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == invalidSecret.Name {
k.SetClientObject(object, &invalidSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, err := BuildOptions(context.TODO(), k, stack, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WithInvalidStorageSchema_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid object storage schema contents: spec does not contain any schemas",
Reason: lokiv1.ReasonInvalidObjectStorageSchema,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
},
Status: lokiv1.LokiStackStatus{
Storage: lokiv1.LokiStackStorageStatus{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
{
Version: lokiv1.ObjectStorageSchemaV12,
EffectiveDate: "2021-10-11",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, err := BuildOptions(context.TODO(), k, stack, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenMissingCAConfigMap_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CASpec: lokiv1.CASpec{
CA: "not-existing",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, err := BuildOptions(context.TODO(), k, stack, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenEmptyCAConfigMapName_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CASpec: lokiv1.CASpec{
CA: "",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, err := BuildOptions(context.TODO(), k, stack, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestBuildOptions_WhenInvalidCAConfigMap_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid object storage CA configmap contents: key not present or data empty: service-ca.crt",
Reason: lokiv1.ReasonInvalidObjectStorageCAConfigMap,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CASpec: lokiv1.CASpec{
CA: invalidCAConfigMap.Name,
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
if name.Name == invalidCAConfigMap.Name {
k.SetClientObject(object, &invalidCAConfigMap)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
_, err := BuildOptions(context.TODO(), k, stack, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"os"
"time"
"github.com/ViaQ/logerr/v2/kverrors"
"github.com/go-logr/logr"
@ -20,22 +19,15 @@ import (
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/handlers/internal/gateway"
"github.com/grafana/loki/operator/internal/handlers/internal/openshift"
"github.com/grafana/loki/operator/internal/handlers/internal/rules"
"github.com/grafana/loki/operator/internal/handlers/internal/serviceaccounts"
"github.com/grafana/loki/operator/internal/handlers/internal/storage"
"github.com/grafana/loki/operator/internal/handlers/internal/tlsprofile"
"github.com/grafana/loki/operator/internal/manifests"
manifests_openshift "github.com/grafana/loki/operator/internal/manifests/openshift"
storageoptions "github.com/grafana/loki/operator/internal/manifests/storage"
"github.com/grafana/loki/operator/internal/metrics"
"github.com/grafana/loki/operator/internal/status"
)
const (
defaultCAKey = "service-ca.crt"
)
// CreateOrUpdateLokiStack handles LokiStack create and update events.
func CreateOrUpdateLokiStack(
ctx context.Context,
@ -67,205 +59,23 @@ func CreateOrUpdateLokiStack(
gwImg = manifests.DefaultLokiStackGatewayImage
}
var storageSecret corev1.Secret
key := client.ObjectKey{Name: stack.Spec.Storage.Secret.Name, Namespace: stack.Namespace}
if err := k.Get(ctx, key, &storageSecret); err != nil {
if apierrors.IsNotFound(err) {
return &status.DegradedError{
Message: "Missing object storage secret",
Reason: lokiv1.ReasonMissingObjectStorageSecret,
Requeue: false,
}
}
return kverrors.Wrap(err, "failed to lookup lokistack storage secret", "name", key)
}
objStore, err := storage.ExtractSecret(&storageSecret, stack.Spec.Storage.Secret.Type)
objStore, err := storage.BuildOptions(ctx, k, &stack, fg)
if err != nil {
return &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage secret contents: %s", err),
Reason: lokiv1.ReasonInvalidObjectStorageSecret,
Requeue: false,
}
return err
}
objStore.OpenShiftEnabled = fg.OpenShift.Enabled
storageSchemas, err := storageoptions.BuildSchemaConfig(
time.Now().UTC(),
stack.Spec.Storage,
stack.Status.Storage,
)
baseDomain, tenants, err := gateway.BuildOptions(ctx, ll, k, &stack, fg)
if err != nil {
return &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage schema contents: %s", err),
Reason: lokiv1.ReasonInvalidObjectStorageSchema,
Requeue: false,
}
}
objStore.Schemas = storageSchemas
if stack.Spec.Storage.TLS != nil {
tlsConfig := stack.Spec.Storage.TLS
if tlsConfig.CA == "" {
return &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
}
var cm corev1.ConfigMap
key := client.ObjectKey{Name: tlsConfig.CA, Namespace: stack.Namespace}
if err = k.Get(ctx, key, &cm); err != nil {
if apierrors.IsNotFound(err) {
return &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
}
return kverrors.Wrap(err, "failed to lookup lokistack object storage CA config map", "name", key)
}
caKey := defaultCAKey
if tlsConfig.CAKey != "" {
caKey = tlsConfig.CAKey
}
var caHash string
caHash, err = storage.CheckCAConfigMap(&cm, caKey)
if err != nil {
return &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage CA configmap contents: %s", err),
Reason: lokiv1.ReasonInvalidObjectStorageCAConfigMap,
Requeue: false,
}
}
objStore.SecretSHA1 = fmt.Sprintf("%s;%s", objStore.SecretSHA1, caHash)
objStore.TLS = &storageoptions.TLSConfig{CA: cm.Name, Key: caKey}
return err
}
var (
baseDomain string
tenantSecrets []*manifests.TenantSecrets
tenantConfigs map[string]manifests.TenantConfig
)
if fg.LokiStackGateway && stack.Spec.Tenants == nil {
return &status.DegradedError{
Message: "Invalid tenants configuration - TenantsSpec cannot be nil when gateway flag is enabled",
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
} else if fg.LokiStackGateway && stack.Spec.Tenants != nil {
if err = gateway.ValidateModes(stack); err != nil {
return &status.DegradedError{
Message: fmt.Sprintf("Invalid tenants configuration: %s", err),
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
}
switch stack.Spec.Tenants.Mode {
case lokiv1.OpenshiftLogging, lokiv1.OpenshiftNetwork:
baseDomain, err = gateway.GetOpenShiftBaseDomain(ctx, k, req)
if err != nil {
return err
}
if stack.Spec.Proxy == nil {
// If the LokiStack has no proxy set but there is a cluster-wide proxy setting,
// set the LokiStack proxy to that.
ocpProxy, proxyErr := openshift.GetProxy(ctx, k)
if proxyErr != nil {
return proxyErr
}
stack.Spec.Proxy = ocpProxy
}
default:
tenantSecrets, err = gateway.GetTenantSecrets(ctx, k, req, &stack)
if err != nil {
return err
}
}
// extract the existing tenant's id, cookieSecret if exists, otherwise create new.
tenantConfigs, err = gateway.GetTenantConfigSecretData(ctx, k, req)
if err != nil {
ll.Error(err, "error in getting tenant secret data")
}
if err = rules.Cleanup(ctx, ll, k, &stack); err != nil {
return err
}
var (
alertingRules []lokiv1.AlertingRule
recordingRules []lokiv1.RecordingRule
rulerConfig *lokiv1.RulerConfigSpec
rulerSecret *manifests.RulerSecret
ocpAmEnabled bool
ocpUWAmEnabled bool
)
if stack.Spec.Rules != nil && stack.Spec.Rules.Enabled {
alertingRules, recordingRules, err = rules.List(ctx, k, req.Namespace, stack.Spec.Rules)
if err != nil {
ll.Error(err, "failed to lookup rules", "spec", stack.Spec.Rules)
}
rulerConfig, err = rules.GetRulerConfig(ctx, k, req)
if err != nil {
ll.Error(err, "failed to lookup ruler config", "key", req.NamespacedName)
}
if rulerConfig != nil && rulerConfig.RemoteWriteSpec != nil && rulerConfig.RemoteWriteSpec.ClientSpec != nil {
var rs corev1.Secret
key := client.ObjectKey{Name: rulerConfig.RemoteWriteSpec.ClientSpec.AuthorizationSecretName, Namespace: stack.Namespace}
if err = k.Get(ctx, key, &rs); err != nil {
if apierrors.IsNotFound(err) {
return &status.DegradedError{
Message: "Missing ruler remote write authorization secret",
Reason: lokiv1.ReasonMissingRulerSecret,
Requeue: false,
}
}
return kverrors.Wrap(err, "failed to lookup lokistack ruler secret", "name", key)
}
rulerSecret, err = rules.ExtractRulerSecret(&rs, rulerConfig.RemoteWriteSpec.ClientSpec.AuthorizationType)
if err != nil {
return &status.DegradedError{
Message: "Invalid ruler remote write authorization secret contents",
Reason: lokiv1.ReasonInvalidRulerSecret,
Requeue: false,
}
}
}
ocpAmEnabled, err = openshift.AlertManagerSVCExists(ctx, stack.Spec, k)
if err != nil {
ll.Error(err, "failed to check OCP AlertManager")
return err
}
ocpUWAmEnabled, err = openshift.UserWorkloadAlertManagerSVCExists(ctx, stack.Spec, k)
if err != nil {
ll.Error(err, "failed to check OCP User Workload AlertManager")
return err
}
} else {
// Clean up ruler resources
err = rules.RemoveRulesConfigMap(ctx, req, k)
if err != nil {
ll.Error(err, "failed to remove rules ConfigMap")
return err
}
err = rules.RemoveRuler(ctx, req, k)
if err != nil {
ll.Error(err, "failed to remove ruler StatefulSet")
return err
}
alertingRules, recordingRules, ruler, ocpOptions, err := rules.BuildOptions(ctx, ll, k, &stack)
if err != nil {
return err
}
certRotationRequiredAt := ""
@ -292,25 +102,14 @@ func CreateOrUpdateLokiStack(
GatewayBaseDomain: baseDomain,
Stack: stack.Spec,
Gates: fg,
ObjectStorage: *objStore,
ObjectStorage: objStore,
CertRotationRequiredAt: certRotationRequiredAt,
AlertingRules: alertingRules,
RecordingRules: recordingRules,
Ruler: manifests.Ruler{
Spec: rulerConfig,
Secret: rulerSecret,
},
Timeouts: timeoutConfig,
Tenants: manifests.Tenants{
Secrets: tenantSecrets,
Configs: tenantConfigs,
},
OpenShiftOptions: manifests_openshift.Options{
BuildOpts: manifests_openshift.BuildOptions{
AlertManagerEnabled: ocpAmEnabled,
UserWorkloadAlertManagerEnabled: ocpUWAmEnabled,
},
},
Ruler: ruler,
Timeouts: timeoutConfig,
Tenants: tenants,
OpenShiftOptions: ocpOptions,
}
ll.Info("begin building manifests")
@ -357,7 +156,7 @@ func CreateOrUpdateLokiStack(
// updated and another resource is not. This would cause the status to
// be possibly misaligned with the configmap, which could lead to
// a user possibly being unable to read logs.
if err := status.SetStorageSchemaStatus(ctx, k, req, storageSchemas); err != nil {
if err := status.SetStorageSchemaStatus(ctx, k, req, objStore.Schemas); err != nil {
ll.Error(err, "failed to set storage schema status")
return err
}

@ -13,7 +13,6 @@ import (
routev1 "github.com/openshift/api/route/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -73,42 +72,6 @@ var (
"issuerCAPath": []byte("/tmp/test/ca.pem"),
},
}
rulesCM = corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack-rules-0",
Namespace: "some-ns",
},
}
rulerSS = appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack-ruler",
Namespace: "some-ns",
},
}
invalidSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
Namespace: "some-ns",
},
Data: map[string][]byte{},
}
invalidCAConfigMap = corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-ca-configmap",
Namespace: "some-ns",
},
Data: map[string]string{},
}
)
func TestMain(m *testing.M) {
@ -573,8 +536,6 @@ func TestCreateOrUpdateLokiStack_WhenCreateReturnsError_ContinueWithOtherObjects
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
@ -681,8 +642,6 @@ func TestCreateOrUpdateLokiStack_WhenUpdateReturnsError_ContinueWithOtherObjects
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
@ -710,69 +669,7 @@ func TestCreateOrUpdateLokiStack_WhenUpdateReturnsError_ContinueWithOtherObjects
require.Error(t, err)
}
func TestCreateOrUpdateLokiStack_WhenMissingSecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing object storage secret",
Reason: lokiv1.ReasonMissingObjectStorageSecret,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenInvalidSecret_SetDegraded(t *testing.T) {
func TestCreateOrUpdateLokiStack_WhenInvalidQueryTimeout_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
@ -783,8 +680,8 @@ func TestCreateOrUpdateLokiStack_WhenInvalidSecret_SetDegraded(t *testing.T) {
}
degradedErr := &status.DegradedError{
Message: "Invalid object storage secret contents: missing secret field",
Reason: lokiv1.ReasonInvalidObjectStorageSecret,
Message: `Error parsing query timeout: time: invalid duration "invalid"`,
Reason: lokiv1.ReasonQueryTimeoutInvalid,
Requeue: false,
}
@ -802,179 +699,37 @@ func TestCreateOrUpdateLokiStack_WhenInvalidSecret_SetDegraded(t *testing.T) {
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
Version: lokiv1.ObjectStorageSchemaV12,
EffectiveDate: "2023-05-22",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: invalidSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == invalidSecret.Name {
k.SetClientObject(object, &invalidSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WithInvalidStorageSchema_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid object storage schema contents: spec does not contain any schemas",
Reason: lokiv1.ReasonInvalidObjectStorageSchema,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
},
Status: lokiv1.LokiStackStatus{
Storage: lokiv1.LokiStackStorageStatus{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
{
Version: lokiv1.ObjectStorageSchemaV12,
EffectiveDate: "2021-10-11",
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "openshift",
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenMissingCAConfigMap_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing object storage CA config map",
Reason: lokiv1.ReasonMissingObjectStorageCAConfigMap,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CASpec: lokiv1.CASpec{
CA: "not-existing",
Limits: &lokiv1.LimitsSpec{
Global: &lokiv1.LimitsTemplateSpec{
QueryLimits: &lokiv1.QueryLimitSpec{
QueryTimeout: "invalid",
},
},
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
// Create looks up the CR first, so we need to return our fake stack
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
if r.Name == name.Name && r.Namespace == name.Namespace {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
return nil
}
k.StatusStub = func() client.StatusWriter { return sw }
@ -985,642 +740,3 @@ func TestCreateOrUpdateLokiStack_WhenMissingCAConfigMap_SetDegraded(t *testing.T
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenInvalidCAConfigMap_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid object storage CA configmap contents: key not present or data empty: service-ca.crt",
Reason: lokiv1.ReasonInvalidObjectStorageCAConfigMap,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CASpec: lokiv1.CASpec{
CA: invalidCAConfigMap.Name,
},
},
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if name.Name == defaultSecret.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
if name.Name == invalidCAConfigMap.Name {
k.SetClientObject(object, &invalidCAConfigMap)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenInvalidTenantsConfiguration_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid tenants configuration: mandatory configuration - missing OPA Url",
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
ff := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: nil,
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
_, isLokiStack := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(object, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, ff)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenMissingGatewaySecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Missing secrets for tenant test",
Reason: lokiv1.ReasonMissingGatewayTenantSecret,
Requeue: true,
}
ff := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
o, ok := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && ok {
k.SetClientObject(o, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, ff)
// make sure error is returned to re-trigger reconciliation
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenInvalidGatewaySecret_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid gateway tenant secret contents",
Reason: lokiv1.ReasonInvalidGatewayTenantSecret,
Requeue: true,
}
ff := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: invalidSecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
o, ok := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && ok {
k.SetClientObject(o, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
if name.Name == invalidSecret.Name {
k.SetClientObject(object, &invalidSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, ff)
// make sure error is returned to re-trigger reconciliation
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_MissingTenantsSpec_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: "Invalid tenants configuration - TenantsSpec cannot be nil when gateway flag is enabled",
Reason: lokiv1.ReasonInvalidTenantsConfiguration,
Requeue: false,
}
ff := configv1.FeatureGates{
LokiStackGateway: true,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: nil,
},
}
// GetStub looks up the CR first, so we need to return our fake stack
// return NotFound for everything else to trigger create.
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
o, ok := object.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && ok {
k.SetClientObject(o, stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, ff)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_WhenInvalidQueryTimeout_SetDegraded(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
degradedErr := &status.DegradedError{
Message: `Error parsing query timeout: time: invalid duration "invalid"`,
Reason: lokiv1.ReasonQueryTimeoutInvalid,
Requeue: false,
}
stack := &lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV12,
EffectiveDate: "2023-05-22",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Tenants: &lokiv1.TenantsSpec{
Mode: "openshift",
},
Limits: &lokiv1.LimitsSpec{
Global: &lokiv1.LimitsTemplateSpec{
QueryLimits: &lokiv1.QueryLimitSpec{
QueryTimeout: "invalid",
},
},
},
},
}
// Create looks up the CR first, so we need to return our fake stack
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object, _ ...client.GetOption) error {
if r.Name == name.Name && r.Namespace == name.Namespace {
k.SetClientObject(object, stack)
}
if defaultSecret.Name == name.Name {
k.SetClientObject(object, &defaultSecret)
}
return nil
}
k.StatusStub = func() client.StatusWriter { return sw }
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
// make sure error is returned
require.Error(t, err)
require.Equal(t, degradedErr, err)
}
func TestCreateOrUpdateLokiStack_RemovesRulerResourcesWhenDisabled(t *testing.T) {
sw := &k8sfakes.FakeStatusWriter{}
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "my-stack",
Namespace: "some-ns",
},
}
stack := lokiv1.LokiStack{
TypeMeta: metav1.TypeMeta{
Kind: "LokiStack",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-stack",
Namespace: "some-ns",
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
},
Spec: lokiv1.LokiStackSpec{
Size: lokiv1.SizeOneXExtraSmall,
Storage: lokiv1.ObjectStorageSpec{
Schemas: []lokiv1.ObjectStorageSchema{
{
Version: lokiv1.ObjectStorageSchemaV11,
EffectiveDate: "2020-10-11",
},
},
Secret: lokiv1.ObjectStorageSecretSpec{
Name: defaultSecret.Name,
Type: lokiv1.ObjectStorageSecretS3,
},
},
Rules: &lokiv1.RulesSpec{
Enabled: true,
},
Tenants: &lokiv1.TenantsSpec{
Mode: "dynamic",
Authentication: []lokiv1.AuthenticationSpec{
{
TenantName: "test",
TenantID: "1234",
OIDC: &lokiv1.OIDCSpec{
Secret: &lokiv1.TenantSecretSpec{
Name: defaultGatewaySecret.Name,
},
},
},
},
Authorization: &lokiv1.AuthorizationSpec{
OPA: &lokiv1.OPASpec{
URL: "some-url",
},
},
},
},
}
k.GetStub = func(_ context.Context, name types.NamespacedName, out client.Object, _ ...client.GetOption) error {
_, ok := out.(*lokiv1.RulerConfig)
if ok {
return apierrors.NewNotFound(schema.GroupResource{}, "no ruler config")
}
_, isLokiStack := out.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(out, &stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(out, &defaultSecret)
return nil
}
if defaultGatewaySecret.Name == name.Name {
k.SetClientObject(out, &defaultGatewaySecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error {
assert.Equal(t, r.Namespace, o.GetNamespace())
return nil
}
k.StatusStub = func() client.StatusWriter { return sw }
k.DeleteStub = func(_ context.Context, o client.Object, _ ...client.DeleteOption) error {
assert.Equal(t, r.Namespace, o.GetNamespace())
return nil
}
k.ListStub = func(_ context.Context, list client.ObjectList, options ...client.ListOption) error {
switch list.(type) {
case *corev1.ConfigMapList:
k.SetClientObjectList(list, &corev1.ConfigMapList{
Items: []corev1.ConfigMap{
rulesCM,
},
})
}
return nil
}
err := CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
require.NoError(t, err)
// make sure create was called
require.NotZero(t, k.CreateCallCount())
// make sure delete not called
require.Zero(t, k.DeleteCallCount())
// Disable the ruler
stack.Spec.Rules.Enabled = false
// Get should return ruler resources
k.GetStub = func(_ context.Context, name types.NamespacedName, out client.Object, _ ...client.GetOption) error {
_, ok := out.(*lokiv1.RulerConfig)
if ok {
return apierrors.NewNotFound(schema.GroupResource{}, "no ruler config")
}
if rulesCM.Name == name.Name {
k.SetClientObject(out, &rulesCM)
return nil
}
if rulerSS.Name == name.Name {
k.SetClientObject(out, &rulerSS)
return nil
}
_, isLokiStack := out.(*lokiv1.LokiStack)
if r.Name == name.Name && r.Namespace == name.Namespace && isLokiStack {
k.SetClientObject(out, &stack)
return nil
}
if defaultSecret.Name == name.Name {
k.SetClientObject(out, &defaultSecret)
return nil
}
if defaultGatewaySecret.Name == name.Name {
k.SetClientObject(out, &defaultGatewaySecret)
return nil
}
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}
err = CreateOrUpdateLokiStack(context.TODO(), logger, r, k, scheme, featureGates)
require.NoError(t, err)
// make sure delete was called twice (delete rules configmap and ruler statefulset)
require.Equal(t, 2, k.DeleteCallCount())
}

Loading…
Cancel
Save