diff --git a/operator/CHANGELOG.md b/operator/CHANGELOG.md
index 2f57c42a78d71..039e37a32297f 100644
--- a/operator/CHANGELOG.md
+++ b/operator/CHANGELOG.md
@@ -1,5 +1,6 @@
## Main
+- [11524](https://github.com/grafana/loki/pull/11524) **JoaoBraveCoding**, **periklis**: Add OpenShift cloud credentials support for AWS STS
- [11513](https://github.com/grafana/loki/pull/11513) **btaani**: Add a custom metric that collects Lokistacks requiring a schema upgrade
- [11718](https://github.com/grafana/loki/pull/11718) **periklis**: Upgrade k8s.io, sigs.k8s.io and openshift deps
- [11671](https://github.com/grafana/loki/pull/11671) **JoaoBraveCoding**: Update mixins to fix structured metadata dashboards
diff --git a/operator/apis/config/v1/projectconfig_types.go b/operator/apis/config/v1/projectconfig_types.go
index 488f7b2cb64f3..ba7cc703c5bb8 100644
--- a/operator/apis/config/v1/projectconfig_types.go
+++ b/operator/apis/config/v1/projectconfig_types.go
@@ -51,6 +51,13 @@ type OpenShiftFeatureGates struct {
// Dashboards enables the loki-mixin dashboards into the OpenShift Console
Dashboards bool `json:"dashboards,omitempty"`
+
+ // ManagedAuthEnv enabled when the operator installation is on OpenShift STS clusters.
+ ManagedAuthEnv bool
+}
+
+func (o OpenShiftFeatureGates) ManagedAuthEnabled() bool {
+ return o.Enabled && o.ManagedAuthEnv
}
// FeatureGates is the supported set of all operator feature gates.
diff --git a/operator/apis/loki/v1/lokistack_types.go b/operator/apis/loki/v1/lokistack_types.go
index 6124c65cd5217..a50fb48b187ea 100644
--- a/operator/apis/loki/v1/lokistack_types.go
+++ b/operator/apis/loki/v1/lokistack_types.go
@@ -1062,6 +1062,12 @@ const (
ReasonMissingObjectStorageSecret LokiStackConditionReason = "MissingObjectStorageSecret"
// ReasonInvalidObjectStorageSecret when the format of the secret is invalid.
ReasonInvalidObjectStorageSecret LokiStackConditionReason = "InvalidObjectStorageSecret"
+ // ReasonMissingCredentialsRequest when the required request for managed auth credentials to object
+ // storage is missing.
+ ReasonMissingCredentialsRequest LokiStackConditionReason = "MissingCredentialsRequest"
+ // ReasonMissingManagedAuthSecret when the required secret for managed auth credentials to object
+ // storage is missing.
+ ReasonMissingManagedAuthSecret LokiStackConditionReason = "MissingManagedAuthenticationSecret"
// ReasonInvalidObjectStorageSchema when the spec contains an invalid schema(s).
ReasonInvalidObjectStorageSchema LokiStackConditionReason = "InvalidObjectStorageSchema"
// ReasonMissingObjectStorageCAConfigMap when the required configmap to verify object storage
diff --git a/operator/bundle/community-openshift/manifests/loki-operator.clusterserviceversion.yaml b/operator/bundle/community-openshift/manifests/loki-operator.clusterserviceversion.yaml
index eef89deddb746..4b20f814804a2 100644
--- a/operator/bundle/community-openshift/manifests/loki-operator.clusterserviceversion.yaml
+++ b/operator/bundle/community-openshift/manifests/loki-operator.clusterserviceversion.yaml
@@ -157,7 +157,7 @@ metadata:
features.operators.openshift.io/fips-compliant: "false"
features.operators.openshift.io/proxy-aware: "true"
features.operators.openshift.io/tls-profiles: "true"
- features.operators.openshift.io/token-auth-aws: "false"
+ features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "false"
features.operators.openshift.io/token-auth-gcp: "false"
operators.operatorframework.io/builder: operator-sdk-unknown
@@ -1463,6 +1463,16 @@ spec:
- patch
- update
- watch
+ - apiGroups:
+ - cloudcredential.openshift.io
+ resources:
+ - credentialsrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - watch
- apiGroups:
- config.openshift.io
resources:
diff --git a/operator/bundle/community/manifests/loki-operator.clusterserviceversion.yaml b/operator/bundle/community/manifests/loki-operator.clusterserviceversion.yaml
index ecc294fddf367..81575be404e82 100644
--- a/operator/bundle/community/manifests/loki-operator.clusterserviceversion.yaml
+++ b/operator/bundle/community/manifests/loki-operator.clusterserviceversion.yaml
@@ -1443,6 +1443,16 @@ spec:
- patch
- update
- watch
+ - apiGroups:
+ - cloudcredential.openshift.io
+ resources:
+ - credentialsrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - watch
- apiGroups:
- config.openshift.io
resources:
diff --git a/operator/bundle/openshift/manifests/loki-operator.clusterserviceversion.yaml b/operator/bundle/openshift/manifests/loki-operator.clusterserviceversion.yaml
index 69e55579d125e..b79f4ea7a2f49 100644
--- a/operator/bundle/openshift/manifests/loki-operator.clusterserviceversion.yaml
+++ b/operator/bundle/openshift/manifests/loki-operator.clusterserviceversion.yaml
@@ -164,7 +164,7 @@ metadata:
features.operators.openshift.io/fips-compliant: "false"
features.operators.openshift.io/proxy-aware: "true"
features.operators.openshift.io/tls-profiles: "true"
- features.operators.openshift.io/token-auth-aws: "false"
+ features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "false"
features.operators.openshift.io/token-auth-gcp: "false"
olm.skipRange: '>=5.7.0-0 <5.9.0'
@@ -1448,6 +1448,16 @@ spec:
- patch
- update
- watch
+ - apiGroups:
+ - cloudcredential.openshift.io
+ resources:
+ - credentialsrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - watch
- apiGroups:
- config.openshift.io
resources:
diff --git a/operator/config/manifests/community-openshift/bases/loki-operator.clusterserviceversion.yaml b/operator/config/manifests/community-openshift/bases/loki-operator.clusterserviceversion.yaml
index c7eb60e5a3e3b..a669b4da3da24 100644
--- a/operator/config/manifests/community-openshift/bases/loki-operator.clusterserviceversion.yaml
+++ b/operator/config/manifests/community-openshift/bases/loki-operator.clusterserviceversion.yaml
@@ -14,7 +14,7 @@ metadata:
features.operators.openshift.io/fips-compliant: "false"
features.operators.openshift.io/proxy-aware: "true"
features.operators.openshift.io/tls-profiles: "true"
- features.operators.openshift.io/token-auth-aws: "false"
+ features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "false"
features.operators.openshift.io/token-auth-gcp: "false"
repository: https://github.com/grafana/loki/tree/main/operator
diff --git a/operator/config/manifests/openshift/bases/loki-operator.clusterserviceversion.yaml b/operator/config/manifests/openshift/bases/loki-operator.clusterserviceversion.yaml
index 5483709ad5d66..0e724292edbb6 100644
--- a/operator/config/manifests/openshift/bases/loki-operator.clusterserviceversion.yaml
+++ b/operator/config/manifests/openshift/bases/loki-operator.clusterserviceversion.yaml
@@ -20,7 +20,7 @@ metadata:
features.operators.openshift.io/fips-compliant: "false"
features.operators.openshift.io/proxy-aware: "true"
features.operators.openshift.io/tls-profiles: "true"
- features.operators.openshift.io/token-auth-aws: "false"
+ features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "false"
features.operators.openshift.io/token-auth-gcp: "false"
olm.skipRange: '>=5.7.0-0 <5.9.0'
diff --git a/operator/config/rbac/role.yaml b/operator/config/rbac/role.yaml
index 09dc60b8c33b9..766a6d7d191e6 100644
--- a/operator/config/rbac/role.yaml
+++ b/operator/config/rbac/role.yaml
@@ -47,6 +47,16 @@ rules:
- patch
- update
- watch
+- apiGroups:
+ - cloudcredential.openshift.io
+ resources:
+ - credentialsrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - watch
- apiGroups:
- config.openshift.io
resources:
diff --git a/operator/controllers/loki/credentialsrequests_controller.go b/operator/controllers/loki/credentialsrequests_controller.go
new file mode 100644
index 0000000000000..61d0b58423e90
--- /dev/null
+++ b/operator/controllers/loki/credentialsrequests_controller.go
@@ -0,0 +1,71 @@
+package controllers
+
+import (
+ "context"
+
+ "github.com/go-logr/logr"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ 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/controllers/loki/internal/lokistack"
+ "github.com/grafana/loki/operator/controllers/loki/internal/management/state"
+ "github.com/grafana/loki/operator/internal/external/k8s"
+ "github.com/grafana/loki/operator/internal/handlers"
+)
+
+// CredentialsRequestsReconciler reconciles a single CredentialsRequest resource for each LokiStack request.
+type CredentialsRequestsReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Log logr.Logger
+}
+
+// Reconcile creates a single CredentialsRequest per LokiStack for the OpenShift cloud-credentials-operator (CCO) to
+// provide a managed cloud credentials Secret. On successful creation, the LokiStack resource is annotated
+// with `loki.grafana.com/credentials-request-secret-ref` that refers to the secret provided by CCO. If the LokiStack
+// resource is not found its accompanying CredentialsRequest resource is deleted.
+func (r *CredentialsRequestsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ var stack lokiv1.LokiStack
+ if err := r.Client.Get(ctx, req.NamespacedName, &stack); err != nil {
+ if apierrors.IsNotFound(err) {
+ return ctrl.Result{}, handlers.DeleteCredentialsRequest(ctx, r.Client, req.NamespacedName)
+ }
+ return ctrl.Result{}, err
+ }
+
+ managed, err := state.IsManaged(ctx, req, r.Client)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ if !managed {
+ r.Log.Info("Skipping reconciliation for unmanaged LokiStack resource", "name", req.String())
+ // Stop requeueing for unmanaged LokiStack custom resources
+ return ctrl.Result{}, nil
+ }
+
+ secretRef, err := handlers.CreateCredentialsRequest(ctx, r.Client, req.NamespacedName)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ if err := lokistack.AnnotateForCredentialsRequest(ctx, r.Client, req.NamespacedName, secretRef); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *CredentialsRequestsReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ b := ctrl.NewControllerManagedBy(mgr)
+ return r.buildController(k8s.NewCtrlBuilder(b))
+}
+
+func (r *CredentialsRequestsReconciler) buildController(bld k8s.Builder) error {
+ return bld.
+ For(&lokiv1.LokiStack{}).
+ Complete(r)
+}
diff --git a/operator/controllers/loki/credentialsrequests_controller_test.go b/operator/controllers/loki/credentialsrequests_controller_test.go
new file mode 100644
index 0000000000000..e6738c1d1796e
--- /dev/null
+++ b/operator/controllers/loki/credentialsrequests_controller_test.go
@@ -0,0 +1,155 @@
+package controllers
+
+import (
+ "context"
+ "testing"
+
+ cloudcredentialsv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
+ "github.com/stretchr/testify/require"
+ 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"
+ "github.com/grafana/loki/operator/internal/manifests/storage"
+)
+
+func TestCredentialsRequestController_RegistersCustomResource_WithDefaultPredicates(t *testing.T) {
+ b := &k8sfakes.FakeBuilder{}
+ k := &k8sfakes.FakeClient{}
+ c := &CredentialsRequestsReconciler{Client: k, Scheme: scheme}
+
+ b.ForReturns(b)
+ b.OwnsReturns(b)
+
+ err := c.buildController(b)
+ require.NoError(t, err)
+
+ // Require only one For-Call for the custom resource
+ require.Equal(t, 1, b.ForCallCount())
+
+ // Require For-call with LokiStack resource
+ obj, _ := b.ForArgsForCall(0)
+ require.Equal(t, &lokiv1.LokiStack{}, obj)
+}
+
+func TestCredentialsRequestController_DeleteCredentialsRequest_WhenLokiStackNotFound(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ c := &CredentialsRequestsReconciler{Client: k, Scheme: scheme}
+ r := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "my-stack",
+ Namespace: "ns",
+ },
+ }
+
+ // Set managed auth environment
+ t.Setenv("ROLEARN", "a-role-arn")
+
+ k.GetStub = func(_ context.Context, key types.NamespacedName, _ client.Object, _ ...client.GetOption) error {
+ if key.Name == r.Name && key.Namespace == r.Namespace {
+ return apierrors.NewNotFound(schema.GroupResource{}, "lokistack not found")
+ }
+ return nil
+ }
+
+ res, err := c.Reconcile(context.Background(), r)
+ require.NoError(t, err)
+ require.Equal(t, ctrl.Result{}, res)
+ require.Equal(t, 1, k.DeleteCallCount())
+}
+
+func TestCredentialsRequestController_CreateCredentialsRequest_WhenLokiStackNotAnnotated(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ c := &CredentialsRequestsReconciler{Client: k, Scheme: scheme}
+ r := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "my-stack",
+ Namespace: "ns",
+ },
+ }
+ s := lokiv1.LokiStack{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-stack",
+ Namespace: "ns",
+ },
+ Spec: lokiv1.LokiStackSpec{
+ ManagementState: lokiv1.ManagementStateManaged,
+ },
+ }
+
+ // Set managed auth environment
+ t.Setenv("ROLEARN", "a-role-arn")
+
+ k.GetStub = func(_ context.Context, key types.NamespacedName, out client.Object, _ ...client.GetOption) error {
+ if key.Name == r.Name && key.Namespace == r.Namespace {
+ k.SetClientObject(out, &s)
+ return nil
+ }
+ return apierrors.NewNotFound(schema.GroupResource{}, "lokistack not found")
+ }
+
+ k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error {
+ _, isCredReq := o.(*cloudcredentialsv1.CredentialsRequest)
+ if !isCredReq {
+ return apierrors.NewBadRequest("something went wrong creating a credentials request")
+ }
+ return nil
+ }
+
+ k.UpdateStub = func(_ context.Context, o client.Object, _ ...client.UpdateOption) error {
+ stack, ok := o.(*lokiv1.LokiStack)
+ if !ok {
+ return apierrors.NewBadRequest("something went wrong creating a credentials request")
+ }
+
+ _, hasSecretRef := stack.Annotations[storage.AnnotationCredentialsRequestsSecretRef]
+ if !hasSecretRef {
+ return apierrors.NewBadRequest("something went updating the lokistack annotations")
+ }
+ return nil
+ }
+
+ res, err := c.Reconcile(context.Background(), r)
+ require.NoError(t, err)
+ require.Equal(t, ctrl.Result{}, res)
+ require.Equal(t, 1, k.CreateCallCount())
+ require.Equal(t, 1, k.UpdateCallCount())
+}
+
+func TestCredentialsRequestController_SkipsUnmanaged(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ c := &CredentialsRequestsReconciler{Client: k, Scheme: scheme}
+ r := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "my-stack",
+ Namespace: "ns",
+ },
+ }
+
+ s := lokiv1.LokiStack{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-stack",
+ Namespace: "ns",
+ },
+ Spec: lokiv1.LokiStackSpec{
+ ManagementState: lokiv1.ManagementStateUnmanaged,
+ },
+ }
+
+ k.GetStub = func(_ context.Context, key types.NamespacedName, out client.Object, _ ...client.GetOption) error {
+ if key.Name == s.Name && key.Namespace == s.Namespace {
+ k.SetClientObject(out, &s)
+ return nil
+ }
+ return apierrors.NewNotFound(schema.GroupResource{}, "something not found")
+ }
+
+ res, err := c.Reconcile(context.Background(), r)
+ require.NoError(t, err)
+ require.Equal(t, ctrl.Result{}, res)
+}
diff --git a/operator/controllers/loki/internal/lokistack/credentialsrequest_discovery.go b/operator/controllers/loki/internal/lokistack/credentialsrequest_discovery.go
new file mode 100644
index 0000000000000..c911c1196eed4
--- /dev/null
+++ b/operator/controllers/loki/internal/lokistack/credentialsrequest_discovery.go
@@ -0,0 +1,30 @@
+package lokistack
+
+import (
+ "context"
+
+ "github.com/ViaQ/logerr/v2/kverrors"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/grafana/loki/operator/internal/external/k8s"
+ "github.com/grafana/loki/operator/internal/manifests/storage"
+)
+
+// AnnotateForCredentialsRequest adds the `loki.grafana.com/credentials-request-secret-ref` annotation
+// to the named Lokistack. If no LokiStack is found, then skip reconciliation. Or else return an error.
+func AnnotateForCredentialsRequest(ctx context.Context, k k8s.Client, key client.ObjectKey, secretRef string) error {
+ stack, err := getLokiStack(ctx, k, key)
+ if stack == nil || err != nil {
+ return err
+ }
+
+ if val, ok := stack.Annotations[storage.AnnotationCredentialsRequestsSecretRef]; ok && val == secretRef {
+ return nil
+ }
+
+ if err := updateAnnotation(ctx, k, stack, storage.AnnotationCredentialsRequestsSecretRef, secretRef); err != nil {
+ return kverrors.Wrap(err, "failed to update lokistack `credentialsRequestSecretRef` annotation", "key", key)
+ }
+
+ return nil
+}
diff --git a/operator/controllers/loki/internal/lokistack/credentialsrequest_discovery_test.go b/operator/controllers/loki/internal/lokistack/credentialsrequest_discovery_test.go
new file mode 100644
index 0000000000000..ef073ca853ba5
--- /dev/null
+++ b/operator/controllers/loki/internal/lokistack/credentialsrequest_discovery_test.go
@@ -0,0 +1,98 @@
+package lokistack
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "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/storage"
+)
+
+func TestAnnotateForCredentialsRequest_ReturnError_WhenLokiStackMissing(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ annotationVal := "ns-my-stack-aws-creds"
+ stackKey := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ k.GetStub = func(_ context.Context, _ types.NamespacedName, out client.Object, _ ...client.GetOption) error {
+ return apierrors.NewBadRequest("failed to get lokistack")
+ }
+
+ err := AnnotateForCredentialsRequest(context.Background(), k, stackKey, annotationVal)
+ require.Error(t, err)
+}
+
+func TestAnnotateForCredentialsRequest_DoNothing_WhenAnnotationExists(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+
+ annotationVal := "ns-my-stack-aws-creds"
+ s := &lokiv1.LokiStack{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-stack",
+ Namespace: "ns",
+ Annotations: map[string]string{
+ storage.AnnotationCredentialsRequestsSecretRef: annotationVal,
+ },
+ },
+ }
+ stackKey := client.ObjectKeyFromObject(s)
+
+ k.GetStub = func(_ context.Context, key types.NamespacedName, out client.Object, _ ...client.GetOption) error {
+ if key.Name == stackKey.Name && key.Namespace == stackKey.Namespace {
+ k.SetClientObject(out, s)
+ return nil
+ }
+ return nil
+ }
+
+ err := AnnotateForCredentialsRequest(context.Background(), k, stackKey, annotationVal)
+ require.NoError(t, err)
+ require.Equal(t, 0, k.UpdateCallCount())
+}
+
+func TestAnnotateForCredentialsRequest_UpdateLokistack_WhenAnnotationMissing(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+
+ annotationVal := "ns-my-stack-aws-creds"
+ s := &lokiv1.LokiStack{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-stack",
+ Namespace: "ns",
+ Annotations: map[string]string{},
+ },
+ }
+ stackKey := client.ObjectKeyFromObject(s)
+
+ k.GetStub = func(_ context.Context, key types.NamespacedName, out client.Object, _ ...client.GetOption) error {
+ if key.Name == stackKey.Name && key.Namespace == stackKey.Namespace {
+ k.SetClientObject(out, s)
+ return nil
+ }
+ return nil
+ }
+
+ k.UpdateStub = func(_ context.Context, o client.Object, _ ...client.UpdateOption) error {
+ stack, ok := o.(*lokiv1.LokiStack)
+ if !ok {
+ return apierrors.NewBadRequest("failed conversion to *lokiv1.LokiStack")
+ }
+ val, ok := stack.Annotations[storage.AnnotationCredentialsRequestsSecretRef]
+ if !ok {
+ return apierrors.NewBadRequest("missing annotation")
+ }
+ if val != annotationVal {
+ return apierrors.NewBadRequest("annotations does not match input")
+ }
+ return nil
+ }
+
+ err := AnnotateForCredentialsRequest(context.Background(), k, stackKey, annotationVal)
+ require.NoError(t, err)
+ require.Equal(t, 1, k.UpdateCallCount())
+}
diff --git a/operator/controllers/loki/lokistack_controller.go b/operator/controllers/loki/lokistack_controller.go
index 629ee85d5edd7..40e7691bd1a2b 100644
--- a/operator/controllers/loki/lokistack_controller.go
+++ b/operator/controllers/loki/lokistack_controller.go
@@ -3,17 +3,20 @@ package controllers
import (
"context"
"errors"
+ "strings"
"time"
"github.com/go-logr/logr"
"github.com/google/go-cmp/cmp"
openshiftconfigv1 "github.com/openshift/api/config/v1"
routev1 "github.com/openshift/api/route/v1"
+ cloudcredentialv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
@@ -30,7 +33,7 @@ import (
"github.com/grafana/loki/operator/controllers/loki/internal/management/state"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/handlers"
- "github.com/grafana/loki/operator/internal/manifests/openshift"
+ manifestsocp "github.com/grafana/loki/operator/internal/manifests/openshift"
"github.com/grafana/loki/operator/internal/status"
)
@@ -125,6 +128,7 @@ type LokiStackReconciler struct {
// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update
// +kubebuilder:rbac:groups=config.openshift.io,resources=dnses;apiservers;proxies,verbs=get;list;watch
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;delete
+// +kubebuilder:rbac:groups=cloudcredential.openshift.io,resources=credentialsrequests,verbs=get;list;watch;create;delete
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
@@ -149,7 +153,7 @@ func (r *LokiStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
err = r.updateResources(ctx, req)
switch {
case errors.As(err, °raded):
- // degraded errors are handled by status.Refresh below
+ // degraded errors are handled by status.Refresh below
case err != nil:
return ctrl.Result{}, err
}
@@ -210,17 +214,19 @@ func (r *LokiStackReconciler) buildController(bld k8s.Builder) error {
}
if r.FeatureGates.OpenShift.Enabled {
- bld = bld.Owns(&routev1.Route{}, updateOrDeleteOnlyPred)
- } else {
- bld = bld.Owns(&networkingv1.Ingress{}, updateOrDeleteOnlyPred)
- }
+ bld = bld.
+ Owns(&routev1.Route{}, updateOrDeleteOnlyPred).
+ Watches(&cloudcredentialv1.CredentialsRequest{}, r.enqueueForCredentialsRequest(), updateOrDeleteOnlyPred)
- if r.FeatureGates.OpenShift.ClusterTLSPolicy {
- bld = bld.Watches(&openshiftconfigv1.APIServer{}, r.enqueueAllLokiStacksHandler(), updateOrDeleteOnlyPred)
- }
+ if r.FeatureGates.OpenShift.ClusterTLSPolicy {
+ bld = bld.Watches(&openshiftconfigv1.APIServer{}, r.enqueueAllLokiStacksHandler(), updateOrDeleteOnlyPred)
+ }
- if r.FeatureGates.OpenShift.ClusterProxy {
- bld = bld.Watches(&openshiftconfigv1.Proxy{}, r.enqueueAllLokiStacksHandler(), updateOrDeleteOnlyPred)
+ if r.FeatureGates.OpenShift.ClusterProxy {
+ bld = bld.Watches(&openshiftconfigv1.Proxy{}, r.enqueueAllLokiStacksHandler(), updateOrDeleteOnlyPred)
+ }
+ } else {
+ bld = bld.Owns(&networkingv1.Ingress{}, updateOrDeleteOnlyPred)
}
return bld.Complete(r)
@@ -271,9 +277,9 @@ func (r *LokiStackReconciler) enqueueForAlertManagerServices() handler.EventHand
}
var requests []reconcile.Request
- if obj.GetName() == openshift.MonitoringSVCOperated &&
- (obj.GetNamespace() == openshift.MonitoringUserWorkloadNS ||
- obj.GetNamespace() == openshift.MonitoringNS) {
+ if obj.GetName() == manifestsocp.MonitoringSVCOperated &&
+ (obj.GetNamespace() == manifestsocp.MonitoringUserWorkloadNS ||
+ obj.GetNamespace() == manifestsocp.MonitoringNS) {
for _, stack := range lokiStacks.Items {
if stack.Spec.Tenants != nil && (stack.Spec.Tenants.Mode == lokiv1.OpenshiftLogging ||
@@ -352,3 +358,34 @@ func (r *LokiStackReconciler) enqueueForStorageCA() handler.EventHandler {
return requests
})
}
+
+func (r *LokiStackReconciler) enqueueForCredentialsRequest() handler.EventHandler {
+ return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+ a := obj.GetAnnotations()
+ owner, ok := a[manifestsocp.AnnotationCredentialsRequestOwner]
+ if !ok {
+ return nil
+ }
+
+ var (
+ ownerParts = strings.Split(owner, "/")
+ namespace = ownerParts[0]
+ name = ownerParts[1]
+ key = client.ObjectKey{Namespace: namespace, Name: name}
+ )
+
+ var stack lokiv1.LokiStack
+ if err := r.Client.Get(ctx, key, &stack); err != nil {
+ if !apierrors.IsNotFound(err) {
+ r.Log.Error(err, "failed retrieving CredentialsRequest owning Lokistack", "key", key)
+ }
+ return nil
+ }
+
+ return []reconcile.Request{
+ {
+ NamespacedName: key,
+ },
+ }
+ })
+}
diff --git a/operator/controllers/loki/lokistack_controller_test.go b/operator/controllers/loki/lokistack_controller_test.go
index 7421b63331b5d..515d829766aa1 100644
--- a/operator/controllers/loki/lokistack_controller_test.go
+++ b/operator/controllers/loki/lokistack_controller_test.go
@@ -10,6 +10,7 @@ import (
"github.com/go-logr/logr"
openshiftconfigv1 "github.com/openshift/api/config/v1"
routev1 "github.com/openshift/api/route/v1"
+ cloudcredentialv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -202,11 +203,23 @@ func TestLokiStackController_RegisterWatchedResources(t *testing.T) {
}
table := []test{
{
- src: &openshiftconfigv1.APIServer{},
+ src: &cloudcredentialv1.CredentialsRequest{},
index: 3,
watchesCallsCount: 4,
featureGates: configv1.FeatureGates{
OpenShift: configv1.OpenShiftFeatureGates{
+ Enabled: true,
+ },
+ },
+ pred: updateOrDeleteOnlyPred,
+ },
+ {
+ src: &openshiftconfigv1.APIServer{},
+ index: 4,
+ watchesCallsCount: 5,
+ featureGates: configv1.FeatureGates{
+ OpenShift: configv1.OpenShiftFeatureGates{
+ Enabled: true,
ClusterTLSPolicy: true,
},
},
@@ -214,10 +227,11 @@ func TestLokiStackController_RegisterWatchedResources(t *testing.T) {
},
{
src: &openshiftconfigv1.Proxy{},
- index: 3,
- watchesCallsCount: 4,
+ index: 4,
+ watchesCallsCount: 5,
featureGates: configv1.FeatureGates{
OpenShift: configv1.OpenShiftFeatureGates{
+ Enabled: true,
ClusterProxy: true,
},
},
diff --git a/operator/docs/operator/api.md b/operator/docs/operator/api.md
index 989a6ef481649..92f93dd970224 100644
--- a/operator/docs/operator/api.md
+++ b/operator/docs/operator/api.md
@@ -1745,6 +1745,10 @@ with the select cluster size.
"InvalidTenantsConfiguration" |
ReasonInvalidTenantsConfiguration when the tenant configuration provided is invalid.
|
+
"MissingCredentialsRequest" |
+ReasonMissingCredentialsRequest when the required request for managed auth credentials to object
+storage is missing.
+ |
"MissingGatewayTenantAuthenticationConfig" |
ReasonMissingGatewayAuthenticationConfig when the config for when a tenant is missing authentication config
|
@@ -1759,6 +1763,10 @@ for authentication is missing.
ReasonMissingGatewayTenantSecret when the required tenant secret
for authentication is missing.
|
+
"MissingManagedAuthenticationSecret" |
+ReasonMissingManagedAuthSecret when the required secret for managed auth credentials to object
+storage is missing.
+ |
"MissingObjectStorageCAConfigMap" |
ReasonMissingObjectStorageCAConfigMap when the required configmap to verify object storage
certificates is missing.
diff --git a/operator/docs/operator/feature-gates.md b/operator/docs/operator/feature-gates.md
index 1d5c046be7755..34fbdf4b69a4d 100644
--- a/operator/docs/operator/feature-gates.md
+++ b/operator/docs/operator/feature-gates.md
@@ -409,6 +409,17 @@ bool
Dashboards enables the loki-mixin dashboards into the OpenShift Console
|
+
+
+ManagedAuthEnv
+
+bool
+
+ |
+
+ ManagedAuthEnv enabled when the operator installation is on OpenShift STS clusters.
+ |
+
diff --git a/operator/go.mod b/operator/go.mod
index 6f02675596bdf..10104f11e38ed 100644
--- a/operator/go.mod
+++ b/operator/go.mod
@@ -12,6 +12,7 @@ require (
github.com/imdario/mergo v0.3.13
github.com/maxbrunsfeld/counterfeiter/v6 v6.7.0
github.com/openshift/api v0.0.0-20240116035456-11ed2fbcb805 // release-4.15
+ github.com/openshift/cloud-credential-operator v0.0.0-20240122210451-67842c7839ac // release-4.15
github.com/openshift/library-go v0.0.0-20240117151256-95b334bccb5d // release-4.15
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.67.1
github.com/prometheus/client_golang v1.17.0
@@ -125,8 +126,8 @@ require (
go.etcd.io/etcd/api/v3 v3.5.9 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect
go.etcd.io/etcd/client/v3 v3.5.9 // indirect
- go.opentelemetry.io/otel v1.11.2 // indirect
- go.opentelemetry.io/otel/trace v1.11.2 // indirect
+ go.opentelemetry.io/otel v1.14.0 // indirect
+ go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/goleak v1.2.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
@@ -136,7 +137,7 @@ require (
golang.org/x/exp v0.0.0-20230124195608-d38c7dcee874 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
- golang.org/x/oauth2 v0.8.0 // indirect
+ golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
@@ -145,12 +146,13 @@ require (
golang.org/x/tools v0.16.1 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
- google.golang.org/grpc v1.56.3 // indirect
+ google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
+ google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.28.3 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
diff --git a/operator/go.sum b/operator/go.sum
index 0d66cc79ec3b4..cfaf2c62e1c57 100644
--- a/operator/go.sum
+++ b/operator/go.sum
@@ -637,12 +637,12 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI=
-github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA=
+github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
-github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8=
+github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
@@ -1020,6 +1020,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/openshift/api v0.0.0-20240116035456-11ed2fbcb805 h1:5NjcOG5i+WH0F4FI8dKSf0fNgX0YQkrJ8w3YcsHx6KM=
github.com/openshift/api v0.0.0-20240116035456-11ed2fbcb805/go.mod h1:qNtV0315F+f8ld52TLtPvrfivZpdimOzTi3kn9IVbtU=
+github.com/openshift/cloud-credential-operator v0.0.0-20240122210451-67842c7839ac h1:ZYatLLVj5pYeNGi9xeebTLfVqdl31MoCa2Jenog1ecM=
+github.com/openshift/cloud-credential-operator v0.0.0-20240122210451-67842c7839ac/go.mod h1:fUDZ7YKd5PC+wFYczavCyHJaw0H3m0WGXNdpFUuN47Q=
github.com/openshift/library-go v0.0.0-20240117151256-95b334bccb5d h1:jDgYsLszzWSgxr0Tas9+L0F2pIu0mngCLv6BA5vubQ4=
github.com/openshift/library-go v0.0.0-20240117151256-95b334bccb5d/go.mod h1:0q1UIvboZXfSlUaK+08wsXYw4N6OUo2b/z3a1EWNGyw=
github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc=
@@ -1180,10 +1182,10 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0=
-go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI=
-go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0=
-go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA=
+go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
+go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
+go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
+go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
@@ -1373,8 +1375,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
-golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
-golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1802,12 +1804,12 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
-google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M=
-google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
-google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
+google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0=
+google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
+google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1850,8 +1852,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
-google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
-google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
+google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1881,7 +1883,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/operator/hack/deploy-aws-storage-secret.sh b/operator/hack/deploy-aws-storage-secret.sh
index ecad7efc5537b..e9689321d0b22 100755
--- a/operator/hack/deploy-aws-storage-secret.sh
+++ b/operator/hack/deploy-aws-storage-secret.sh
@@ -1,4 +1,22 @@
#!/usr/bin/env bash
+#
+# usage: deploy-aws-storage-secret.sh ()
+#
+# This scripts deploys a LokiStack Secret resource holding the
+# authentication credentials to access AWS S3. It supports three
+# modes: static authentication, managed with custom role_arn and
+# fully managed by OpeShift's Cloud-Credentials-Operator. To use
+# one of the managed you need to pass the environment variable
+# STS=true. If you pass the second optional argument you can set
+# your custom managed role_arn.
+#
+# bucket_name is the name of the bucket to be used in the LokiStack
+# object storage secret.
+#
+# role_arn is the ARN value of the upfront manually provisioned AWS
+# Role that grants access to the and it's object on
+# AWS S3.
+#
set -euo pipefail
@@ -12,15 +30,33 @@ fi
readonly namespace=${NAMESPACE:-openshift-logging}
region=${REGION:-$(aws configure get region)}
readonly region
+
+# static authentication from the current select AWS CLI profile.
access_key_id=${ACCESS_KEY_ID:-$(aws configure get aws_access_key_id)}
readonly access_key_id
secret_access_key=${SECRET_ACCESS_KEY:-$(aws configure get aws_secret_access_key)}
readonly secret_access_key
-kubectl --ignore-not-found=true -n "${namespace}" delete secret test
-kubectl -n "${namespace}" create secret generic test \
+# Managed authentication with/without a manually provisioned AWS Role.
+readonly sts=${STS:-false}
+readonly role_arn=${2-}
+
+create_secret_args=( \
--from-literal=region="$(echo -n "${region}")" \
--from-literal=bucketnames="$(echo -n "${bucket_name}")" \
- --from-literal=access_key_id="$(echo -n "${access_key_id}")" \
- --from-literal=access_key_secret="$(echo -n "${secret_access_key}")" \
- --from-literal=endpoint="$(echo -n "https://s3.${region}.amazonaws.com")"
+)
+
+if [[ "${sts}" = "true" ]]; then
+ if [[ -n "${role_arn}" ]]; then
+ create_secret_args+=(--from-literal=role_arn="$(echo -n "${role_arn}")")
+ fi
+else
+ create_secret_args+=( \
+ --from-literal=access_key_id="$(echo -n "${access_key_id}")" \
+ --from-literal=access_key_secret="$(echo -n "${secret_access_key}")" \
+ --from-literal=endpoint="$(echo -n "https://s3.${region}.amazonaws.com")" \
+ )
+fi
+
+kubectl --ignore-not-found=true -n "${namespace}" delete secret test
+kubectl -n "${namespace}" create secret generic test "${create_secret_args[@]}"
diff --git a/operator/internal/handlers/credentialsrequest_create.go b/operator/internal/handlers/credentialsrequest_create.go
new file mode 100644
index 0000000000000..477528326b9a5
--- /dev/null
+++ b/operator/internal/handlers/credentialsrequest_create.go
@@ -0,0 +1,42 @@
+package handlers
+
+import (
+ "context"
+
+ "github.com/ViaQ/logerr/v2/kverrors"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/grafana/loki/operator/internal/external/k8s"
+ "github.com/grafana/loki/operator/internal/manifests/openshift"
+)
+
+// CreateCredentialsRequest creates a new CredentialsRequest resource for a Lokistack
+// to request a cloud credentials Secret resource from the OpenShift cloud-credentials-operator.
+func CreateCredentialsRequest(ctx context.Context, k k8s.Client, stack client.ObjectKey) (string, error) {
+ managedAuthEnv := openshift.DiscoverManagedAuthEnv()
+ if managedAuthEnv == nil {
+ return "", nil
+ }
+
+ opts := openshift.Options{
+ BuildOpts: openshift.BuildOptions{
+ LokiStackName: stack.Name,
+ LokiStackNamespace: stack.Namespace,
+ },
+ ManagedAuthEnv: managedAuthEnv,
+ }
+
+ credReq, err := openshift.BuildCredentialsRequest(opts)
+ if err != nil {
+ return "", err
+ }
+
+ if err := k.Create(ctx, credReq); err != nil {
+ if !apierrors.IsAlreadyExists(err) {
+ return "", kverrors.Wrap(err, "failed to create credentialsrequest", "key", client.ObjectKeyFromObject(credReq))
+ }
+ }
+
+ return credReq.Spec.SecretRef.Name, nil
+}
diff --git a/operator/internal/handlers/credentialsrequest_create_test.go b/operator/internal/handlers/credentialsrequest_create_test.go
new file mode 100644
index 0000000000000..f6bf9c0f1b526
--- /dev/null
+++ b/operator/internal/handlers/credentialsrequest_create_test.go
@@ -0,0 +1,50 @@
+package handlers
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
+)
+
+func TestCreateCredentialsRequest_DoNothing_WhenManagedAuthEnvMissing(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ key := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ secretRef, err := CreateCredentialsRequest(context.Background(), k, key)
+ require.NoError(t, err)
+ require.Empty(t, secretRef)
+}
+
+func TestCreateCredentialsRequest_CreateNewResource(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ key := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ t.Setenv("ROLEARN", "a-role-arn")
+
+ secretRef, err := CreateCredentialsRequest(context.Background(), k, key)
+ require.NoError(t, err)
+ require.NotEmpty(t, secretRef)
+ require.Equal(t, 1, k.CreateCallCount())
+}
+
+func TestCreateCredentialsRequest_DoNothing_WhenCredentialsRequestExist(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ key := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ t.Setenv("ROLEARN", "a-role-arn")
+
+ k.CreateStub = func(_ context.Context, _ client.Object, _ ...client.CreateOption) error {
+ return errors.NewAlreadyExists(schema.GroupResource{}, "credentialsrequest exists")
+ }
+
+ secretRef, err := CreateCredentialsRequest(context.Background(), k, key)
+ require.NoError(t, err)
+ require.NotEmpty(t, secretRef)
+ require.Equal(t, 1, k.CreateCallCount())
+}
diff --git a/operator/internal/handlers/credentialsrequest_delete.go b/operator/internal/handlers/credentialsrequest_delete.go
new file mode 100644
index 0000000000000..edf05fcb205d0
--- /dev/null
+++ b/operator/internal/handlers/credentialsrequest_delete.go
@@ -0,0 +1,43 @@
+package handlers
+
+import (
+ "context"
+
+ "github.com/ViaQ/logerr/v2/kverrors"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/grafana/loki/operator/internal/external/k8s"
+ "github.com/grafana/loki/operator/internal/manifests/openshift"
+)
+
+// DeleteCredentialsRequest deletes a LokiStack's accompanying CredentialsRequest resource
+// to trigger the OpenShift cloud-credentials-operator to wipe out any credentials related
+// Secret resource on the LokiStack namespace.
+func DeleteCredentialsRequest(ctx context.Context, k k8s.Client, stack client.ObjectKey) error {
+ managedAuthEnv := openshift.DiscoverManagedAuthEnv()
+ if managedAuthEnv == nil {
+ return nil
+ }
+
+ opts := openshift.Options{
+ BuildOpts: openshift.BuildOptions{
+ LokiStackName: stack.Name,
+ LokiStackNamespace: stack.Namespace,
+ },
+ ManagedAuthEnv: managedAuthEnv,
+ }
+
+ credReq, err := openshift.BuildCredentialsRequest(opts)
+ if err != nil {
+ return kverrors.Wrap(err, "failed to build credentialsrequest", "key", stack)
+ }
+
+ if err := k.Delete(ctx, credReq); err != nil {
+ if !errors.IsNotFound(err) {
+ return kverrors.Wrap(err, "failed to delete credentialsrequest", "key", client.ObjectKeyFromObject(credReq))
+ }
+ }
+
+ return nil
+}
diff --git a/operator/internal/handlers/credentialsrequest_delete_test.go b/operator/internal/handlers/credentialsrequest_delete_test.go
new file mode 100644
index 0000000000000..57f1c005ee706
--- /dev/null
+++ b/operator/internal/handlers/credentialsrequest_delete_test.go
@@ -0,0 +1,47 @@
+package handlers
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/grafana/loki/operator/internal/external/k8s/k8sfakes"
+)
+
+func TestDeleteCredentialsRequest_DoNothing_WhenManagedAuthEnvMissing(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ key := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ err := DeleteCredentialsRequest(context.Background(), k, key)
+ require.NoError(t, err)
+}
+
+func TestDeleteCredentialsRequest_DeleteExistingResource(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ key := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ t.Setenv("ROLEARN", "a-role-arn")
+
+ err := DeleteCredentialsRequest(context.Background(), k, key)
+ require.NoError(t, err)
+ require.Equal(t, 1, k.DeleteCallCount())
+}
+
+func TestDeleteCredentialsRequest_DoNothing_WhenCredentialsRequestNotExists(t *testing.T) {
+ k := &k8sfakes.FakeClient{}
+ key := client.ObjectKey{Name: "my-stack", Namespace: "ns"}
+
+ t.Setenv("ROLEARN", "a-role-arn")
+
+ k.DeleteStub = func(_ context.Context, _ client.Object, _ ...client.DeleteOption) error {
+ return errors.NewNotFound(schema.GroupResource{}, "credentials request not found")
+ }
+
+ err := DeleteCredentialsRequest(context.Background(), k, key)
+ require.NoError(t, err)
+ require.Equal(t, 1, k.DeleteCallCount())
+}
diff --git a/operator/internal/handlers/internal/storage/secrets.go b/operator/internal/handlers/internal/storage/secrets.go
index 0ef5f197a625e..705cabb6cf5d9 100644
--- a/operator/internal/handlers/internal/storage/secrets.go
+++ b/operator/internal/handlers/internal/storage/secrets.go
@@ -11,6 +11,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"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"
"github.com/grafana/loki/operator/internal/manifests/storage"
@@ -19,47 +20,92 @@ import (
var hashSeparator = []byte(",")
-func getSecret(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack) (*corev1.Secret, error) {
- var storageSecret corev1.Secret
+func getSecrets(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (*corev1.Secret, *corev1.Secret, error) {
+ var (
+ storageSecret corev1.Secret
+ managedAuthSecret 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{
+ return nil, 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 nil, nil, kverrors.Wrap(err, "failed to lookup lokistack storage secret", "name", key)
+ }
+
+ if fg.OpenShift.ManagedAuthEnv {
+ secretName, ok := stack.Annotations[storage.AnnotationCredentialsRequestsSecretRef]
+ if !ok {
+ return nil, nil, &status.DegradedError{
+ Message: "Missing OpenShift cloud credentials request",
+ Reason: lokiv1.ReasonMissingCredentialsRequest,
+ Requeue: true,
+ }
+ }
+
+ managedAuthCredsKey := client.ObjectKey{Name: secretName, Namespace: stack.Namespace}
+ if err := k.Get(ctx, managedAuthCredsKey, &managedAuthSecret); err != nil {
+ if apierrors.IsNotFound(err) {
+ return nil, nil, &status.DegradedError{
+ Message: "Missing OpenShift cloud credentials secret",
+ Reason: lokiv1.ReasonMissingManagedAuthSecret,
+ Requeue: true,
+ }
+ }
+ return nil, nil, kverrors.Wrap(err, "failed to lookup OpenShift CCO managed authentication credentials secret", "name", stack)
+ }
+
+ return &storageSecret, &managedAuthSecret, nil
}
- return &storageSecret, nil
+ return &storageSecret, nil, 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)
+// extractSecrets reads the k8s obj storage secret into a manifest object storage struct if valid.
+// The managed auth is also read into the manifest object under the right circumstances.
+func extractSecrets(secretType lokiv1.ObjectStorageSecretType, objStore, managedAuth *corev1.Secret, fg configv1.FeatureGates) (storage.Options, error) {
+ hash, err := hashSecretData(objStore)
if err != nil {
return storage.Options{}, kverrors.Wrap(err, "error calculating hash for secret", "type", secretType)
}
storageOpts := storage.Options{
- SecretName: s.Name,
+ SecretName: objStore.Name,
SecretSHA1: hash,
SharedStore: secretType,
}
+ if fg.OpenShift.ManagedAuthEnabled() {
+ var managedAuthHash string
+ managedAuthHash, err = hashSecretData(managedAuth)
+ if err != nil {
+ return storage.Options{}, kverrors.Wrap(err, "error calculating hash for secret", "type", client.ObjectKeyFromObject(managedAuth))
+ }
+
+ storageOpts.OpenShift = storage.OpenShiftOptions{
+ CloudCredentials: storage.CloudCredentials{
+ SecretName: managedAuth.Name,
+ SHA1: managedAuthHash,
+ },
+ }
+ }
+
switch secretType {
case lokiv1.ObjectStorageSecretAzure:
- storageOpts.Azure, err = extractAzureConfigSecret(s)
+ storageOpts.Azure, err = extractAzureConfigSecret(objStore)
case lokiv1.ObjectStorageSecretGCS:
- storageOpts.GCS, err = extractGCSConfigSecret(s)
+ storageOpts.GCS, err = extractGCSConfigSecret(objStore)
case lokiv1.ObjectStorageSecretS3:
- storageOpts.S3, err = extractS3ConfigSecret(s)
+ storageOpts.S3, err = extractS3ConfigSecret(objStore, fg)
case lokiv1.ObjectStorageSecretSwift:
- storageOpts.Swift, err = extractSwiftConfigSecret(s)
+ storageOpts.Swift, err = extractSwiftConfigSecret(objStore)
case lokiv1.ObjectStorageSecretAlibabaCloud:
- storageOpts.AlibabaCloud, err = extractAlibabaCloudConfigSecret(s)
+ storageOpts.AlibabaCloud, err = extractAlibabaCloudConfigSecret(objStore)
default:
return storage.Options{}, kverrors.New("unknown secret type", "type", secretType)
}
@@ -146,7 +192,7 @@ func extractGCSConfigSecret(s *corev1.Secret) (*storage.GCSStorageConfig, error)
}, nil
}
-func extractS3ConfigSecret(s *corev1.Secret) (*storage.S3StorageConfig, error) {
+func extractS3ConfigSecret(s *corev1.Secret, fg configv1.FeatureGates) (*storage.S3StorageConfig, error) {
// Extract and validate mandatory fields
buckets := s.Data[storage.KeyAWSBucketNames]
if len(buckets) == 0 {
@@ -176,8 +222,29 @@ func extractS3ConfigSecret(s *corev1.Secret) (*storage.S3StorageConfig, error) {
SSE: sseCfg,
}
+ var (
+ isManagedAuthEnv = len(roleArn) != 0
+ isStaticAuthEnv = !isManagedAuthEnv
+ )
+
switch {
- case len(roleArn) == 0:
+ case fg.OpenShift.ManagedAuthEnabled():
+ cfg.STS = true
+ cfg.Audience = storage.AWSOpenShiftAudience
+ // Do not allow users overriding the role arn provided on Loki Operator installation
+ if len(roleArn) != 0 {
+ return nil, kverrors.New("extra secret field set", "field", storage.KeyAWSRoleArn)
+ }
+ if len(audience) != 0 {
+ return nil, kverrors.New("extra secret field set", "field", storage.KeyAWSAudience)
+ }
+ // In the STS case region is not an optional field
+ if len(region) == 0 {
+ return nil, kverrors.New("missing secret field", "field", storage.KeyAWSRegion)
+ }
+
+ return cfg, nil
+ case isStaticAuthEnv:
cfg.Endpoint = string(endpoint)
if len(endpoint) == 0 {
@@ -191,8 +258,7 @@ func extractS3ConfigSecret(s *corev1.Secret) (*storage.S3StorageConfig, error) {
}
return cfg, nil
- // TODO(JoaoBraveCoding) For CCO integration here we will first check if we get a secret, OS use-case
- case len(roleArn) != 0: // Extract STS from user provided values
+ case isManagedAuthEnv: // Extract STS from user provided values
cfg.STS = true
cfg.Audience = string(audience)
diff --git a/operator/internal/handlers/internal/storage/secrets_test.go b/operator/internal/handlers/internal/storage/secrets_test.go
index c72c63ea1ee12..535fd3a0aa141 100644
--- a/operator/internal/handlers/internal/storage/secrets_test.go
+++ b/operator/internal/handlers/internal/storage/secrets_test.go
@@ -7,6 +7,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ configv1 "github.com/grafana/loki/operator/apis/config/v1"
lokiv1 "github.com/grafana/loki/operator/apis/loki/v1"
)
@@ -135,7 +136,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 := extractSecrets(lokiv1.ObjectStorageSecretAzure, tst.secret, nil, configv1.FeatureGates{})
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
@@ -186,7 +187,7 @@ func TestGCSExtract(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()
- _, err := extractSecret(tst.secret, lokiv1.ObjectStorageSecretGCS)
+ _, err := extractSecrets(lokiv1.ObjectStorageSecretGCS, tst.secret, nil, configv1.FeatureGates{})
if !tst.wantErr {
require.NoError(t, err)
}
@@ -360,7 +361,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 := extractSecrets(lokiv1.ObjectStorageSecretS3, tst.secret, nil, configv1.FeatureGates{})
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
@@ -374,6 +375,80 @@ func TestS3Extract(t *testing.T) {
}
}
+func TestS3Extract_WithOpenShiftManagedAuth(t *testing.T) {
+ fg := configv1.FeatureGates{
+ OpenShift: configv1.OpenShiftFeatureGates{
+ Enabled: true,
+ ManagedAuthEnv: true,
+ },
+ }
+ type test struct {
+ name string
+ secret *corev1.Secret
+ managedAuthSecret *corev1.Secret
+ wantErr bool
+ }
+ table := []test{
+ {
+ name: "missing role-arn",
+ secret: &corev1.Secret{},
+ managedAuthSecret: &corev1.Secret{},
+ wantErr: true,
+ },
+ {
+ name: "missing region",
+ secret: &corev1.Secret{},
+ managedAuthSecret: &corev1.Secret{},
+ wantErr: true,
+ },
+ {
+ name: "override role arn not allowed",
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "test"},
+ Data: map[string][]byte{
+ "role_arn": []byte("role-arn"),
+ },
+ },
+ managedAuthSecret: &corev1.Secret{},
+ wantErr: true,
+ },
+ {
+ name: "STS all set",
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "test"},
+ Data: map[string][]byte{
+ "bucketnames": []byte("this,that"),
+ "region": []byte("a-region"),
+ },
+ },
+ managedAuthSecret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "managed-auth"},
+ },
+ },
+ }
+ for _, tst := range table {
+ tst := tst
+ t.Run(tst.name, func(t *testing.T) {
+ t.Parallel()
+
+ opts, err := extractSecrets(lokiv1.ObjectStorageSecretS3, tst.secret, tst.managedAuthSecret, fg)
+ if !tst.wantErr {
+ require.NoError(t, err)
+ require.NotEmpty(t, opts.SecretName)
+ require.NotEmpty(t, opts.SecretSHA1)
+ require.Equal(t, opts.SharedStore, lokiv1.ObjectStorageSecretS3)
+ require.True(t, opts.S3.STS)
+ require.Equal(t, opts.S3.Audience, "openshift")
+ require.Equal(t, opts.OpenShift.CloudCredentials.SecretName, tst.managedAuthSecret.Name)
+ require.NotEmpty(t, opts.OpenShift.CloudCredentials.SHA1)
+ }
+ if tst.wantErr {
+ require.NotNil(t, err)
+ }
+ })
+ }
+}
+
func TestSwiftExtract(t *testing.T) {
type test struct {
name string
@@ -509,7 +584,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 := extractSecrets(lokiv1.ObjectStorageSecretSwift, tst.secret, nil, configv1.FeatureGates{})
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
@@ -583,7 +658,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 := extractSecrets(lokiv1.ObjectStorageSecretAlibabaCloud, tst.secret, nil, configv1.FeatureGates{})
if !tst.wantErr {
require.NoError(t, err)
require.NotEmpty(t, opts.SecretName)
diff --git a/operator/internal/handlers/internal/storage/storage.go b/operator/internal/handlers/internal/storage/storage.go
index e1657121ccd6d..32b59522ef6a5 100644
--- a/operator/internal/handlers/internal/storage/storage.go
+++ b/operator/internal/handlers/internal/storage/storage.go
@@ -20,13 +20,14 @@ import (
// - 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.
+// - The object storage managed auth secret is missing (Only on OpenShift STS-clusters)
func BuildOptions(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (storage.Options, error) {
- storageSecret, err := getSecret(ctx, k, stack)
+ storageSecret, managedAuthSecret, err := getSecrets(ctx, k, stack, fg)
if err != nil {
return storage.Options{}, err
}
- objStore, err := extractSecret(storageSecret, stack.Spec.Storage.Secret.Type)
+ objStore, err := extractSecrets(stack.Spec.Storage.Secret.Type, storageSecret, managedAuthSecret, fg)
if err != nil {
return storage.Options{}, &status.DegradedError{
Message: fmt.Sprintf("Invalid object storage secret contents: %s", err),
@@ -34,7 +35,7 @@ func BuildOptions(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg
Requeue: false,
}
}
- objStore.OpenShiftEnabled = fg.OpenShift.Enabled
+ objStore.OpenShift.Enabled = fg.OpenShift.Enabled
storageSchemas, err := storage.BuildSchemaConfig(
time.Now().UTC(),
diff --git a/operator/internal/handlers/internal/storage/storage_test.go b/operator/internal/handlers/internal/storage/storage_test.go
index f56e446d6da8f..9bc73630b2dc2 100644
--- a/operator/internal/handlers/internal/storage/storage_test.go
+++ b/operator/internal/handlers/internal/storage/storage_test.go
@@ -2,6 +2,7 @@ package storage
import (
"context"
+ "fmt"
"testing"
"github.com/stretchr/testify/require"
@@ -16,6 +17,7 @@ import (
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/manifests/storage"
"github.com/grafana/loki/operator/internal/status"
)
@@ -46,6 +48,16 @@ var (
},
}
+ defaultManagedAuthSecret = corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-stack-secret",
+ Namespace: "some-ns",
+ },
+ Data: map[string][]byte{
+ "region": []byte("a-region"),
+ },
+ }
+
invalidSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some-stack-secret",
@@ -123,6 +135,153 @@ func TestBuildOptions_WhenMissingSecret_SetDegraded(t *testing.T) {
require.Equal(t, degradedErr, err)
}
+func TestBuildOptions_WhenMissingCloudCredentialsRequest_SetDegraded(t *testing.T) {
+ sw := &k8sfakes.FakeStatusWriter{}
+ k := &k8sfakes.FakeClient{}
+ r := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "my-stack",
+ Namespace: "some-ns",
+ },
+ }
+
+ fg := configv1.FeatureGates{
+ OpenShift: configv1.OpenShiftFeatureGates{
+ ManagedAuthEnv: true,
+ },
+ }
+
+ degradedErr := &status.DegradedError{
+ Message: "Missing OpenShift cloud credentials request",
+ Reason: lokiv1.ReasonMissingCredentialsRequest,
+ Requeue: true,
+ }
+
+ stack := &lokiv1.LokiStack{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "LokiStack",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-stack",
+ Namespace: "some-ns",
+ UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
+ Annotations: map[string]string{},
+ },
+ Spec: lokiv1.LokiStackSpec{
+ Size: lokiv1.SizeOneXExtraSmall,
+ Storage: lokiv1.ObjectStorageSpec{
+ Schemas: []lokiv1.ObjectStorageSchema{
+ {
+ Version: lokiv1.ObjectStorageSchemaV11,
+ EffectiveDate: "2020-10-11",
+ },
+ },
+ Secret: lokiv1.ObjectStorageSecretSpec{
+ Name: defaultManagedAuthSecret.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 == defaultManagedAuthSecret.Name {
+ k.SetClientObject(object, &defaultManagedAuthSecret)
+ return nil
+ }
+ return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
+ }
+
+ k.StatusStub = func() client.StatusWriter { return sw }
+
+ _, err := BuildOptions(context.TODO(), k, stack, fg)
+
+ // make sure error is returned
+ require.Error(t, err)
+ require.Equal(t, degradedErr, err)
+}
+
+func TestBuildOptions_WhenMissingCloudCredentialsSecret_SetDegraded(t *testing.T) {
+ sw := &k8sfakes.FakeStatusWriter{}
+ k := &k8sfakes.FakeClient{}
+ r := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "my-stack",
+ Namespace: "some-ns",
+ },
+ }
+
+ fg := configv1.FeatureGates{
+ OpenShift: configv1.OpenShiftFeatureGates{
+ ManagedAuthEnv: true,
+ },
+ }
+
+ degradedErr := &status.DegradedError{
+ Message: "Missing OpenShift cloud credentials secret",
+ Reason: lokiv1.ReasonMissingManagedAuthSecret,
+ Requeue: true,
+ }
+
+ stack := &lokiv1.LokiStack{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "LokiStack",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-stack",
+ Namespace: "some-ns",
+ UID: "b23f9a38-9672-499f-8c29-15ede74d3ece",
+ Annotations: map[string]string{
+ storage.AnnotationCredentialsRequestsSecretRef: "my-stack-aws-creds",
+ },
+ },
+ Spec: lokiv1.LokiStackSpec{
+ Size: lokiv1.SizeOneXExtraSmall,
+ Storage: lokiv1.ObjectStorageSpec{
+ Schemas: []lokiv1.ObjectStorageSchema{
+ {
+ Version: lokiv1.ObjectStorageSchemaV11,
+ EffectiveDate: "2020-10-11",
+ },
+ },
+ Secret: lokiv1.ObjectStorageSecretSpec{
+ Name: defaultManagedAuthSecret.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 == defaultManagedAuthSecret.Name {
+ k.SetClientObject(object, &defaultManagedAuthSecret)
+ return nil
+ }
+ if name.Name == fmt.Sprintf("%s-aws-creds", stack.Name) {
+ return apierrors.NewNotFound(schema.GroupResource{}, "cloud credentials auth secret is not found")
+ }
+ return apierrors.NewNotFound(schema.GroupResource{}, "something is not found")
+ }
+
+ k.StatusStub = func() client.StatusWriter { return sw }
+
+ _, err := BuildOptions(context.TODO(), k, stack, fg)
+
+ // 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{}
diff --git a/operator/internal/manifests/compactor.go b/operator/internal/manifests/compactor.go
index 0c5c6b038a1cf..24d95945cf0ac 100644
--- a/operator/internal/manifests/compactor.go
+++ b/operator/internal/manifests/compactor.go
@@ -67,7 +67,7 @@ func BuildCompactor(opts Options) ([]client.Object, error) {
// NewCompactorStatefulSet creates a statefulset object for a compactor.
func NewCompactorStatefulSet(opts Options) *appsv1.StatefulSet {
l := ComponentLabels(LabelCompactorComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
ServiceAccountName: opts.Name,
Affinity: configureAffinity(LabelCompactorComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.Compactor),
diff --git a/operator/internal/manifests/distributor.go b/operator/internal/manifests/distributor.go
index 7b5a0a033f19a..ca84da935982a 100644
--- a/operator/internal/manifests/distributor.go
+++ b/operator/internal/manifests/distributor.go
@@ -67,7 +67,7 @@ func BuildDistributor(opts Options) ([]client.Object, error) {
// NewDistributorDeployment creates a deployment object for a distributor
func NewDistributorDeployment(opts Options) *appsv1.Deployment {
l := ComponentLabels(LabelDistributorComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
ServiceAccountName: opts.Name,
Affinity: configureAffinity(LabelDistributorComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.Distributor),
diff --git a/operator/internal/manifests/gateway.go b/operator/internal/manifests/gateway.go
index 1ba3a9905e577..b3809ed9c296c 100644
--- a/operator/internal/manifests/gateway.go
+++ b/operator/internal/manifests/gateway.go
@@ -114,7 +114,7 @@ func BuildGateway(opts Options) ([]client.Object, error) {
// NewGatewayDeployment creates a deployment object for a lokiStack-gateway
func NewGatewayDeployment(opts Options, sha1C string) *appsv1.Deployment {
l := ComponentLabels(LabelGatewayComponent, opts.Name)
- a := commonAnnotations(sha1C, "", opts.CertRotationRequiredAt)
+ a := gatewayAnnotations(sha1C, opts.CertRotationRequiredAt)
podSpec := corev1.PodSpec{
ServiceAccountName: GatewayName(opts.Name),
Affinity: configureAffinity(LabelGatewayComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.Gateway),
diff --git a/operator/internal/manifests/indexgateway.go b/operator/internal/manifests/indexgateway.go
index f4dbbe8f6f248..171598cc2822e 100644
--- a/operator/internal/manifests/indexgateway.go
+++ b/operator/internal/manifests/indexgateway.go
@@ -73,7 +73,7 @@ func BuildIndexGateway(opts Options) ([]client.Object, error) {
// NewIndexGatewayStatefulSet creates a statefulset object for an index-gateway
func NewIndexGatewayStatefulSet(opts Options) *appsv1.StatefulSet {
l := ComponentLabels(LabelIndexGatewayComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
ServiceAccountName: opts.Name,
Affinity: configureAffinity(LabelIndexGatewayComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.IndexGateway),
diff --git a/operator/internal/manifests/ingester.go b/operator/internal/manifests/ingester.go
index 6e7a50af4806e..5aabb3abfe73a 100644
--- a/operator/internal/manifests/ingester.go
+++ b/operator/internal/manifests/ingester.go
@@ -73,7 +73,7 @@ func BuildIngester(opts Options) ([]client.Object, error) {
// NewIngesterStatefulSet creates a deployment object for an ingester
func NewIngesterStatefulSet(opts Options) *appsv1.StatefulSet {
l := ComponentLabels(LabelIngesterComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
ServiceAccountName: opts.Name,
Affinity: configureAffinity(LabelIngesterComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.Ingester),
diff --git a/operator/internal/manifests/openshift/credentialsrequest.go b/operator/internal/manifests/openshift/credentialsrequest.go
new file mode 100644
index 0000000000000..8fc2c5d3f5129
--- /dev/null
+++ b/operator/internal/manifests/openshift/credentialsrequest.go
@@ -0,0 +1,96 @@
+package openshift
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/ViaQ/logerr/v2/kverrors"
+ cloudcredentialv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/grafana/loki/operator/internal/manifests/storage"
+)
+
+const (
+ ccoNamespace = "openshift-cloud-credential-operator"
+)
+
+func BuildCredentialsRequest(opts Options) (*cloudcredentialv1.CredentialsRequest, error) {
+ stack := client.ObjectKey{Name: opts.BuildOpts.LokiStackName, Namespace: opts.BuildOpts.LokiStackNamespace}
+
+ providerSpec, secretName, err := encodeProviderSpec(opts.BuildOpts.LokiStackName, opts.ManagedAuthEnv)
+ if err != nil {
+ return nil, kverrors.Wrap(err, "failed encoding credentialsrequest provider spec")
+ }
+
+ return &cloudcredentialv1.CredentialsRequest{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("%s-%s", stack.Namespace, secretName),
+ Namespace: ccoNamespace,
+ Annotations: map[string]string{
+ AnnotationCredentialsRequestOwner: stack.String(),
+ },
+ },
+ Spec: cloudcredentialv1.CredentialsRequestSpec{
+ SecretRef: corev1.ObjectReference{
+ Name: secretName,
+ Namespace: stack.Namespace,
+ },
+ ProviderSpec: providerSpec,
+ ServiceAccountNames: []string{
+ stack.Name,
+ },
+ CloudTokenPath: path.Join(storage.SATokenVolumeOcpDirectory, "token"),
+ },
+ }, nil
+}
+
+func encodeProviderSpec(stackName string, env *ManagedAuthEnv) (*runtime.RawExtension, string, error) {
+ var (
+ spec runtime.Object
+ secretName string
+ )
+
+ switch {
+ case env.AWS != nil:
+ spec = &cloudcredentialv1.AWSProviderSpec{
+ StatementEntries: []cloudcredentialv1.StatementEntry{
+ {
+ Action: []string{
+ "s3:ListBucket",
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ },
+ Effect: "Allow",
+ Resource: "arn:aws:s3:*:*:*",
+ },
+ },
+ STSIAMRoleARN: env.AWS.RoleARN,
+ }
+ secretName = fmt.Sprintf("%s-aws-creds", stackName)
+ }
+
+ encodedSpec, err := cloudcredentialv1.Codec.EncodeProviderSpec(spec.DeepCopyObject())
+ return encodedSpec, secretName, err
+}
+
+func DiscoverManagedAuthEnv() *ManagedAuthEnv {
+ // AWS
+ roleARN := os.Getenv("ROLEARN")
+
+ switch {
+ case roleARN != "":
+ return &ManagedAuthEnv{
+ AWS: &AWSSTSEnv{
+ RoleARN: roleARN,
+ },
+ }
+ }
+
+ return nil
+}
diff --git a/operator/internal/manifests/openshift/credentialsrequest_test.go b/operator/internal/manifests/openshift/credentialsrequest_test.go
new file mode 100644
index 0000000000000..0672cadfc210f
--- /dev/null
+++ b/operator/internal/manifests/openshift/credentialsrequest_test.go
@@ -0,0 +1,119 @@
+package openshift
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/grafana/loki/operator/internal/manifests/storage"
+)
+
+func TestBuildCredentialsRequest_HasOwnerAnnotation(t *testing.T) {
+ opts := Options{
+ BuildOpts: BuildOptions{
+ LokiStackName: "a-stack",
+ LokiStackNamespace: "ns",
+ },
+ ManagedAuthEnv: &ManagedAuthEnv{
+ AWS: &AWSSTSEnv{
+ RoleARN: "role-arn",
+ },
+ },
+ }
+
+ credReq, err := BuildCredentialsRequest(opts)
+ require.NoError(t, err)
+ require.Contains(t, credReq.Annotations, AnnotationCredentialsRequestOwner)
+}
+
+func TestBuildCredentialsRequest_HasSecretRef_MatchingLokiStackNamespace(t *testing.T) {
+ opts := Options{
+ BuildOpts: BuildOptions{
+ LokiStackName: "a-stack",
+ LokiStackNamespace: "ns",
+ },
+ ManagedAuthEnv: &ManagedAuthEnv{
+ AWS: &AWSSTSEnv{
+ RoleARN: "role-arn",
+ },
+ },
+ }
+
+ credReq, err := BuildCredentialsRequest(opts)
+ require.NoError(t, err)
+ require.Equal(t, opts.BuildOpts.LokiStackNamespace, credReq.Spec.SecretRef.Namespace)
+}
+
+func TestBuildCredentialsRequest_HasServiceAccountNames_ContainsLokiStackName(t *testing.T) {
+ opts := Options{
+ BuildOpts: BuildOptions{
+ LokiStackName: "a-stack",
+ LokiStackNamespace: "ns",
+ },
+ ManagedAuthEnv: &ManagedAuthEnv{
+ AWS: &AWSSTSEnv{
+ RoleARN: "role-arn",
+ },
+ },
+ }
+
+ credReq, err := BuildCredentialsRequest(opts)
+ require.NoError(t, err)
+ require.Contains(t, credReq.Spec.ServiceAccountNames, opts.BuildOpts.LokiStackName)
+}
+
+func TestBuildCredentialsRequest_CloudTokenPath_MatchinOpenShiftSADirectory(t *testing.T) {
+ opts := Options{
+ BuildOpts: BuildOptions{
+ LokiStackName: "a-stack",
+ LokiStackNamespace: "ns",
+ },
+ ManagedAuthEnv: &ManagedAuthEnv{
+ AWS: &AWSSTSEnv{
+ RoleARN: "role-arn",
+ },
+ },
+ }
+
+ credReq, err := BuildCredentialsRequest(opts)
+ require.NoError(t, err)
+ require.True(t, strings.HasPrefix(credReq.Spec.CloudTokenPath, storage.SATokenVolumeOcpDirectory))
+}
+
+func TestBuildCredentialsRequest_FollowsNamingConventions(t *testing.T) {
+ tests := []struct {
+ desc string
+ opts Options
+ wantName string
+ wantSecretName string
+ }{
+ {
+ desc: "aws",
+ opts: Options{
+ BuildOpts: BuildOptions{
+ LokiStackName: "a-stack",
+ LokiStackNamespace: "ns",
+ },
+ ManagedAuthEnv: &ManagedAuthEnv{
+ AWS: &AWSSTSEnv{
+ RoleARN: "role-arn",
+ },
+ },
+ },
+ wantName: "ns-a-stack-aws-creds",
+ wantSecretName: "a-stack-aws-creds",
+ },
+ }
+ for _, test := range tests {
+ test := test
+ t.Run(test.desc, func(t *testing.T) {
+ t.Parallel()
+
+ credReq, err := BuildCredentialsRequest(test.opts)
+ require.NoError(t, err)
+ require.Equal(t, test.wantName, credReq.Name)
+ require.Equal(t, test.wantSecretName, credReq.Spec.SecretRef.Name)
+ })
+ }
+}
diff --git a/operator/internal/manifests/openshift/options.go b/operator/internal/manifests/openshift/options.go
index 2ebf5ebde1f46..e5d33a3355269 100644
--- a/operator/internal/manifests/openshift/options.go
+++ b/operator/internal/manifests/openshift/options.go
@@ -14,6 +14,7 @@ type Options struct {
BuildOpts BuildOptions
Authentication []AuthenticationSpec
Authorization AuthorizationSpec
+ ManagedAuthEnv *ManagedAuthEnv
}
// AuthenticationSpec describes the authentication specification
@@ -54,6 +55,14 @@ type TenantData struct {
CookieSecret string
}
+type AWSSTSEnv struct {
+ RoleARN string
+}
+
+type ManagedAuthEnv struct {
+ AWS *AWSSTSEnv
+}
+
// NewOptions returns an openshift options struct.
func NewOptions(
stackName, stackNamespace string,
diff --git a/operator/internal/manifests/openshift/var.go b/operator/internal/manifests/openshift/var.go
index 5e3ac6300e3eb..84928c48d7e28 100644
--- a/operator/internal/manifests/openshift/var.go
+++ b/operator/internal/manifests/openshift/var.go
@@ -48,6 +48,8 @@ var (
MonitoringSVCUserWorkload = "alertmanager-user-workload"
MonitoringUserWorkloadNS = "openshift-user-workload-monitoring"
+
+ AnnotationCredentialsRequestOwner = "loki.grafana.com/credentialsrequest-owner"
)
func authorizerRbacName(componentName string) string {
diff --git a/operator/internal/manifests/querier.go b/operator/internal/manifests/querier.go
index c807fe8ed1f0d..f98de94a060ea 100644
--- a/operator/internal/manifests/querier.go
+++ b/operator/internal/manifests/querier.go
@@ -73,7 +73,7 @@ func BuildQuerier(opts Options) ([]client.Object, error) {
// NewQuerierDeployment creates a deployment object for a querier
func NewQuerierDeployment(opts Options) *appsv1.Deployment {
l := ComponentLabels(LabelQuerierComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
ServiceAccountName: opts.Name,
Affinity: configureAffinity(LabelQuerierComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.Querier),
diff --git a/operator/internal/manifests/query-frontend.go b/operator/internal/manifests/query-frontend.go
index e1023e872371c..7786470020cb1 100644
--- a/operator/internal/manifests/query-frontend.go
+++ b/operator/internal/manifests/query-frontend.go
@@ -67,7 +67,7 @@ func BuildQueryFrontend(opts Options) ([]client.Object, error) {
// NewQueryFrontendDeployment creates a deployment object for a query-frontend
func NewQueryFrontendDeployment(opts Options) *appsv1.Deployment {
l := ComponentLabels(LabelQueryFrontendComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
ServiceAccountName: opts.Name,
Affinity: configureAffinity(LabelQueryFrontendComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.QueryFrontend),
diff --git a/operator/internal/manifests/ruler.go b/operator/internal/manifests/ruler.go
index c34adb765ee71..4e9eaf22b66b0 100644
--- a/operator/internal/manifests/ruler.go
+++ b/operator/internal/manifests/ruler.go
@@ -97,7 +97,7 @@ func NewRulerStatefulSet(opts Options) *appsv1.StatefulSet {
}
l := ComponentLabels(LabelRulerComponent, opts.Name)
- a := commonAnnotations(opts.ConfigSHA1, opts.ObjectStorage.SecretSHA1, opts.CertRotationRequiredAt)
+ a := commonAnnotations(opts)
podSpec := corev1.PodSpec{
Affinity: configureAffinity(LabelRulerComponent, opts.Name, opts.Gates.DefaultNodeAffinity, opts.Stack.Template.Ruler),
Volumes: []corev1.Volume{
diff --git a/operator/internal/manifests/storage/configure.go b/operator/internal/manifests/storage/configure.go
index 06956827db420..6f7b22c4bd8ce 100644
--- a/operator/internal/manifests/storage/configure.go
+++ b/operator/internal/manifests/storage/configure.go
@@ -131,6 +131,11 @@ func ensureObjectStoreCredentials(p *corev1.PodSpec, opts Options) corev1.PodSpe
container.Env = append(container.Env, managedAuthCredentials(opts)...)
volumes = append(volumes, saTokenVolume(opts))
container.VolumeMounts = append(container.VolumeMounts, saTokenVolumeMount(opts))
+
+ if opts.OpenShift.ManagedAuthEnabled() {
+ volumes = append(volumes, managedAuthVolume(opts))
+ container.VolumeMounts = append(container.VolumeMounts, managedAuthVolumeMount(opts))
+ }
} else {
container.Env = append(container.Env, staticAuthCredentials(opts)...)
}
@@ -179,9 +184,16 @@ func staticAuthCredentials(opts Options) []corev1.EnvVar {
func managedAuthCredentials(opts Options) []corev1.EnvVar {
switch opts.SharedStore {
case lokiv1.ObjectStorageSecretS3:
- return []corev1.EnvVar{
- envVarFromSecret(EnvAWSRoleArn, opts.SecretName, KeyAWSRoleArn),
- envVarFromValue(EnvAWSWebIdentityTokenFile, path.Join(opts.S3.WebIdentityTokenFile, "token")),
+ if opts.OpenShift.ManagedAuthEnabled() {
+ return []corev1.EnvVar{
+ envVarFromValue(EnvAWSCredentialsFile, path.Join(managedAuthSecretDirectory, KeyAWSCredentialsFilename)),
+ envVarFromValue(EnvAWSSdkLoadConfig, "true"),
+ }
+ } else {
+ return []corev1.EnvVar{
+ envVarFromSecret(EnvAWSRoleArn, opts.SecretName, KeyAWSRoleArn),
+ envVarFromValue(EnvAWSWebIdentityTokenFile, path.Join(opts.S3.WebIdentityTokenFile, "token")),
+ }
}
default:
return []corev1.EnvVar{}
@@ -270,8 +282,8 @@ func setSATokenPath(opts *Options) {
switch opts.SharedStore {
case lokiv1.ObjectStorageSecretS3:
opts.S3.WebIdentityTokenFile = saTokenVolumeK8sDirectory
- if opts.OpenShiftEnabled {
- opts.S3.WebIdentityTokenFile = saTokenVolumeOcpDirectory
+ if opts.OpenShift.Enabled {
+ opts.S3.WebIdentityTokenFile = SATokenVolumeOcpDirectory
}
}
}
@@ -297,8 +309,8 @@ func saTokenVolume(opts Options) corev1.Volume {
if opts.S3.Audience != "" {
audience = opts.S3.Audience
}
- if opts.OpenShiftEnabled {
- audience = awsOpenShiftAudience
+ if opts.OpenShift.Enabled {
+ audience = AWSOpenShiftAudience
}
}
return corev1.Volume{
@@ -318,3 +330,21 @@ func saTokenVolume(opts Options) corev1.Volume {
},
}
}
+
+func managedAuthVolumeMount(opts Options) corev1.VolumeMount {
+ return corev1.VolumeMount{
+ Name: opts.OpenShift.CloudCredentials.SecretName,
+ MountPath: managedAuthSecretDirectory,
+ }
+}
+
+func managedAuthVolume(opts Options) corev1.Volume {
+ return corev1.Volume{
+ Name: opts.OpenShift.CloudCredentials.SecretName,
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: opts.OpenShift.CloudCredentials.SecretName,
+ },
+ },
+ }
+}
diff --git a/operator/internal/manifests/storage/configure_test.go b/operator/internal/manifests/storage/configure_test.go
index 220d0c6c701a6..3b3029733554d 100644
--- a/operator/internal/manifests/storage/configure_test.go
+++ b/operator/internal/manifests/storage/configure_test.go
@@ -393,13 +393,19 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
{
desc: "object storage S3 in STS Mode in OpenShift",
opts: Options{
- SecretName: "test",
- OpenShiftEnabled: true,
- SharedStore: lokiv1.ObjectStorageSecretS3,
+ SecretName: "test",
+ SharedStore: lokiv1.ObjectStorageSecretS3,
S3: &S3StorageConfig{
STS: true,
Audience: "test",
},
+ OpenShift: OpenShiftOptions{
+ Enabled: true,
+ CloudCredentials: CloudCredentials{
+ SecretName: "cloud-credentials",
+ SHA1: "deadbeef",
+ },
+ },
},
dpl: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
@@ -432,22 +438,20 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
ReadOnly: false,
MountPath: "/var/run/secrets/openshift/serviceaccount",
},
+ {
+ Name: "cloud-credentials",
+ ReadOnly: false,
+ MountPath: "/etc/storage/managed-auth",
+ },
},
Env: []corev1.EnvVar{
{
- Name: EnvAWSRoleArn,
- ValueFrom: &corev1.EnvVarSource{
- SecretKeyRef: &corev1.SecretKeySelector{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "test",
- },
- Key: KeyAWSRoleArn,
- },
- },
+ Name: "AWS_SHARED_CREDENTIALS_FILE",
+ Value: "/etc/storage/managed-auth/credentials",
},
{
- Name: "AWS_WEB_IDENTITY_TOKEN_FILE",
- Value: "/var/run/secrets/openshift/serviceaccount/token",
+ Name: "AWS_SDK_LOAD_CONFIG",
+ Value: "true",
},
},
},
@@ -477,6 +481,14 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
},
},
},
+ {
+ Name: "cloud-credentials",
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: "cloud-credentials",
+ },
+ },
+ },
},
},
},
@@ -948,6 +960,111 @@ func TestConfigureStatefulSetForStorageType(t *testing.T) {
},
},
},
+ {
+ desc: "object storage S3 in STS Mode in OpenShift",
+ opts: Options{
+ SecretName: "test",
+ SharedStore: lokiv1.ObjectStorageSecretS3,
+ S3: &S3StorageConfig{
+ STS: true,
+ Audience: "test",
+ },
+ OpenShift: OpenShiftOptions{
+ Enabled: true,
+ CloudCredentials: CloudCredentials{
+ SecretName: "cloud-credentials",
+ SHA1: "deadbeef",
+ },
+ },
+ },
+ sts: &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "loki-ingester",
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "loki-ingester",
+ VolumeMounts: []corev1.VolumeMount{
+ {
+ Name: "test",
+ ReadOnly: false,
+ MountPath: "/etc/storage/secrets",
+ },
+ {
+ Name: saTokenVolumeName,
+ ReadOnly: false,
+ MountPath: "/var/run/secrets/openshift/serviceaccount",
+ },
+ {
+ Name: "cloud-credentials",
+ ReadOnly: false,
+ MountPath: "/etc/storage/managed-auth",
+ },
+ },
+ Env: []corev1.EnvVar{
+ {
+ Name: "AWS_SHARED_CREDENTIALS_FILE",
+ Value: "/etc/storage/managed-auth/credentials",
+ },
+ {
+ Name: "AWS_SDK_LOAD_CONFIG",
+ Value: "true",
+ },
+ },
+ },
+ },
+ Volumes: []corev1.Volume{
+ {
+ Name: "test",
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: "test",
+ },
+ },
+ },
+ {
+ Name: saTokenVolumeName,
+ VolumeSource: corev1.VolumeSource{
+ Projected: &corev1.ProjectedVolumeSource{
+ Sources: []corev1.VolumeProjection{
+ {
+ ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
+ Audience: "openshift",
+ ExpirationSeconds: ptr.To[int64](3600),
+ Path: corev1.ServiceAccountTokenKey,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "cloud-credentials",
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: "cloud-credentials",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
{
desc: "object storage S3 with SSE KMS encryption Context",
opts: Options{
diff --git a/operator/internal/manifests/storage/options.go b/operator/internal/manifests/storage/options.go
index 7ecf7f78b4258..80efb24f62c8b 100644
--- a/operator/internal/manifests/storage/options.go
+++ b/operator/internal/manifests/storage/options.go
@@ -16,10 +16,11 @@ type Options struct {
Swift *SwiftStorageConfig
AlibabaCloud *AlibabaCloudStorageConfig
- SecretName string
- SecretSHA1 string
- TLS *TLSConfig
- OpenShiftEnabled bool
+ SecretName string
+ SecretSHA1 string
+ TLS *TLSConfig
+
+ OpenShift OpenShiftOptions
}
// AzureStorageConfig for Azure storage config
@@ -86,3 +87,17 @@ type TLSConfig struct {
CA string
Key string
}
+
+type OpenShiftOptions struct {
+ Enabled bool
+ CloudCredentials CloudCredentials
+}
+
+type CloudCredentials struct {
+ SecretName string
+ SHA1 string
+}
+
+func (o OpenShiftOptions) ManagedAuthEnabled() bool {
+ return o.CloudCredentials.SecretName != "" && o.CloudCredentials.SHA1 != ""
+}
diff --git a/operator/internal/manifests/storage/var.go b/operator/internal/manifests/storage/var.go
index 16b7e10d3d1b5..d77de3262d314 100644
--- a/operator/internal/manifests/storage/var.go
+++ b/operator/internal/manifests/storage/var.go
@@ -15,6 +15,10 @@ const (
EnvAWSRoleArn = "AWS_ROLE_ARN"
// EnvAWSWebIdentityToken is the environment variable to specify the path to the web identity token file used in the federated identity workflow.
EnvAWSWebIdentityTokenFile = "AWS_WEB_IDENTITY_TOKEN_FILE"
+ // EnvAWSCredentialsFile is the environment variable to specify the path to the shared credentials file
+ EnvAWSCredentialsFile = "AWS_SHARED_CREDENTIALS_FILE"
+ // EnvAWSSdkLoadConfig is the environment that enabled the AWS SDK to enable the shared credentials file to be loaded
+ EnvAWSSdkLoadConfig = "AWS_SDK_LOAD_CONFIG"
// EnvAzureStorageAccountName is the environment variable to specify the Azure storage account name to access the container.
EnvAzureStorageAccountName = "AZURE_STORAGE_ACCOUNT_NAME"
// EnvAzureStorageAccountKey is the environment variable to specify the Azure storage account key to access the container.
@@ -55,6 +59,8 @@ const (
KeyAWSRoleArn = "role_arn"
// KeyAWSAudience is the audience for the AWS STS workflow.
KeyAWSAudience = "audience"
+ // KeyAWSCredentialsFilename is the config filename containing the AWS authentication credentials.
+ KeyAWSCredentialsFilename = "credentials"
// KeyAzureStorageAccountKey is the secret data key for the Azure storage account key.
KeyAzureStorageAccountKey = "account_key"
@@ -102,14 +108,17 @@ const (
KeySwiftUsername = "username"
saTokenVolumeK8sDirectory = "/var/run/secrets/kubernetes.io/serviceaccount"
- saTokenVolumeOcpDirectory = "/var/run/secrets/openshift/serviceaccount"
+ SATokenVolumeOcpDirectory = "/var/run/secrets/openshift/serviceaccount"
saTokenVolumeName = "bound-sa-token"
saTokenExpiration int64 = 3600
- secretDirectory = "/etc/storage/secrets"
- storageTLSVolume = "storage-tls"
- caDirectory = "/etc/storage/ca"
+ secretDirectory = "/etc/storage/secrets"
+ managedAuthSecretDirectory = "/etc/storage/managed-auth"
+ storageTLSVolume = "storage-tls"
+ caDirectory = "/etc/storage/ca"
awsDefaultAudience = "sts.amazonaws.com"
- awsOpenShiftAudience = "openshift"
+ AWSOpenShiftAudience = "openshift"
+
+ AnnotationCredentialsRequestsSecretRef = "loki.grafana.com/credentials-request-secret-ref"
)
diff --git a/operator/internal/manifests/var.go b/operator/internal/manifests/var.go
index 1395412b792da..cffb30d0cf900 100644
--- a/operator/internal/manifests/var.go
+++ b/operator/internal/manifests/var.go
@@ -78,6 +78,8 @@ const (
AnnotationLokiConfigHash string = "loki.grafana.com/config-hash"
// AnnotationLokiObjectStoreHash stores the last SHA1 hash of the loki object storage credetials.
AnnotationLokiObjectStoreHash string = "loki.grafana.com/object-store-hash"
+ // AnnotationLokiManagedAuthHash stores the last SHA1 hash of the loki managed auth credentials.
+ AnnotationLokiManagedAuthHash string = "loki.grafana.com/managed-auth-hash"
// LabelCompactorComponent is the label value for the compactor component
LabelCompactorComponent string = "compactor"
@@ -133,20 +135,30 @@ var (
volumeFileSystemMode = corev1.PersistentVolumeFilesystem
)
-func commonAnnotations(configHash, objStoreHash, rotationRequiredAt string) map[string]string {
+func commonAnnotations(opts Options) map[string]string {
a := map[string]string{
- AnnotationLokiConfigHash: configHash,
+ AnnotationLokiConfigHash: opts.ConfigSHA1,
+ AnnotationCertRotationRequiredAt: opts.CertRotationRequiredAt,
+ }
- AnnotationCertRotationRequiredAt: rotationRequiredAt,
+ if opts.ObjectStorage.SecretSHA1 != "" {
+ a[AnnotationLokiObjectStoreHash] = opts.ObjectStorage.SecretSHA1
}
- if objStoreHash != "" {
- a[AnnotationLokiObjectStoreHash] = objStoreHash
+ if opts.ObjectStorage.OpenShift.CloudCredentials.SHA1 != "" {
+ a[AnnotationLokiManagedAuthHash] = opts.ObjectStorage.OpenShift.CloudCredentials.SHA1
}
return a
}
+func gatewayAnnotations(configSHA1, certRotationRequiredAt string) map[string]string {
+ return map[string]string{
+ AnnotationLokiConfigHash: configSHA1,
+ AnnotationCertRotationRequiredAt: certRotationRequiredAt,
+ }
+}
+
func commonLabels(stackName string) map[string]string {
return map[string]string{
"app.kubernetes.io/name": "lokistack",
diff --git a/operator/main.go b/operator/main.go
index 654cd11d9f0a7..a88a857bcee44 100644
--- a/operator/main.go
+++ b/operator/main.go
@@ -8,6 +8,7 @@ import (
"github.com/ViaQ/logerr/v2/log"
configv1 "github.com/openshift/api/config/v1"
routev1 "github.com/openshift/api/route/v1"
+ cloudcredentialv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -20,6 +21,7 @@ import (
lokiv1beta1 "github.com/grafana/loki/operator/apis/loki/v1beta1"
lokictrl "github.com/grafana/loki/operator/controllers/loki"
"github.com/grafana/loki/operator/internal/config"
+ manifestsocp "github.com/grafana/loki/operator/internal/manifests/openshift"
"github.com/grafana/loki/operator/internal/metrics"
"github.com/grafana/loki/operator/internal/operator"
"github.com/grafana/loki/operator/internal/validation"
@@ -83,6 +85,7 @@ func main() {
if ctrlCfg.Gates.OpenShift.Enabled {
utilruntime.Must(routev1.AddToScheme(scheme))
+ utilruntime.Must(cloudcredentialv1.AddToScheme(scheme))
}
}
@@ -92,6 +95,11 @@ func main() {
os.Exit(1)
}
+ if ctrlCfg.Gates.OpenShift.Enabled && manifestsocp.DiscoverManagedAuthEnv() != nil {
+ logger.Info("discovered OpenShift Cluster within a managed authentication environment")
+ ctrlCfg.Gates.OpenShift.ManagedAuthEnv = true
+ }
+
if err = (&lokictrl.LokiStackReconciler{
Client: mgr.GetClient(),
Log: logger.WithName("controllers").WithName("lokistack"),
@@ -121,6 +129,17 @@ func main() {
}
}
+ if ctrlCfg.Gates.OpenShift.ManagedAuthEnabled() {
+ if err = (&lokictrl.CredentialsRequestsReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Log: logger.WithName("controllers").WithName("lokistack-credentialsrequest"),
+ }).SetupWithManager(mgr); err != nil {
+ logger.Error(err, "unable to create controller", "controller", "lokistack-credentialsrequest")
+ os.Exit(1)
+ }
+ }
+
if ctrlCfg.Gates.LokiStackWebhook {
v := &validation.LokiStackValidator{}
if err = v.SetupWebhookWithManager(mgr); err != nil {