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 {