Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

operator: Add support for running with Google Workload Identity #11869

Merged
merged 9 commits into from
Feb 8, 2024
1 change: 1 addition & 0 deletions operator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Main

- [11869](https://github.com/grafana/loki/pull/11869) **periklis**: Add support for running with Google Workload Identity
- [11854](https://github.com/grafana/loki/pull/11854) **periklis**: Allow custom audience for managed-auth on STS
- [11802](https://github.com/grafana/loki/pull/11802) **xperimental**: Add support for running with Azure Workload Identity
- [11824](https://github.com/grafana/loki/pull/11824) **xperimental**: Improve messages for errors in storage secret
Expand Down
26 changes: 25 additions & 1 deletion operator/internal/handlers/internal/storage/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package storage
import (
"context"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"sort"
Expand Down Expand Up @@ -31,8 +32,12 @@ var (

errAzureNoCredentials = errors.New("azure storage secret does contain neither account_key or client_id")
errAzureMixedCredentials = errors.New("azure storage secret can not contain both account_key and client_id")

errGCPParsingCredentialsFile = errors.New("gcp storage secret cannot be parsed from JSON content")
periklis marked this conversation as resolved.
Show resolved Hide resolved
)

const gcpExternalAccountType = "external_account"
periklis marked this conversation as resolved.
Show resolved Hide resolved

func getSecrets(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (*corev1.Secret, *corev1.Secret, error) {
var (
storageSecret corev1.Secret
Expand Down Expand Up @@ -233,8 +238,27 @@ func extractGCSConfigSecret(s *corev1.Secret) (*storage.GCSStorageConfig, error)
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPServiceAccountKeyFilename)
}

credentialsFile := struct {
CredentialsType string `json:"type"`
}{}
periklis marked this conversation as resolved.
Show resolved Hide resolved

err := json.Unmarshal(keyJSON, &credentialsFile)
if err != nil {
return nil, errGCPParsingCredentialsFile
periklis marked this conversation as resolved.
Show resolved Hide resolved
}

var (
audience = string(s.Data[storage.KeyGCPWorkloadIdentityProviderAudience])
isWorkloadIdentity = credentialsFile.CredentialsType == gcpExternalAccountType
periklis marked this conversation as resolved.
Show resolved Hide resolved
)
if isWorkloadIdentity && len(audience) == 0 {
periklis marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPWorkloadIdentityProviderAudience)
periklis marked this conversation as resolved.
Show resolved Hide resolved
}

return &storage.GCSStorageConfig{
Bucket: string(bucket),
Bucket: string(bucket),
WorkloadIdentity: isWorkloadIdentity,
Audience: audience,
}, nil
}

Expand Down
24 changes: 23 additions & 1 deletion operator/internal/handlers/internal/storage/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,35 @@ func TestGCSExtract(t *testing.T) {
},
wantError: "missing secret field: key.json",
},
{
name: "missing audience",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"key.json": []byte("{\"type\": \"external_account\"}"),
},
},
wantError: "missing secret field: audience",
},
{
name: "all set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"key.json": []byte("{\"type\": \"SA\"}"),
"key.json": []byte("{\"type\": \"service_account\"}"),
},
},
},
{
name: "mandatory for workload-identity set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"audience": []byte("test"),
"key.json": []byte("{\"type\": \"external_account\"}"),
},
},
},
Expand Down
10 changes: 10 additions & 0 deletions operator/internal/manifests/storage/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ func managedAuthCredentials(opts Options) []corev1.EnvVar {
envVarFromSecret(EnvAzureSubscriptionID, opts.SecretName, KeyAzureStorageSubscriptionID),
envVarFromValue(EnvAzureFederatedTokenFile, path.Join(azureTokenVolumeDirectory, "token")),
}
case lokiv1.ObjectStorageSecretGCS:
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(secretDirectory, KeyGCPServiceAccountKeyFilename)),
}
default:
return []corev1.EnvVar{}
}
Expand Down Expand Up @@ -280,6 +284,8 @@ func managedAuthEnabled(opts Options) bool {
return opts.S3 != nil && opts.S3.STS
case lokiv1.ObjectStorageSecretAzure:
return opts.Azure != nil && opts.Azure.WorkloadIdentity
case lokiv1.ObjectStorageSecretGCS:
return opts.GCS != nil && opts.GCS.WorkloadIdentity
default:
return false
}
Expand All @@ -292,6 +298,8 @@ func saTokenVolumeMount(opts Options) corev1.VolumeMount {
tokenPath = AWSTokenVolumeDirectory
case lokiv1.ObjectStorageSecretAzure:
tokenPath = azureTokenVolumeDirectory
case lokiv1.ObjectStorageSecretGCS:
tokenPath = gcpTokenVolumeDirectory
}
return corev1.VolumeMount{
Name: saTokenVolumeName,
Expand All @@ -310,6 +318,8 @@ func saTokenVolume(opts Options) corev1.Volume {
}
case lokiv1.ObjectStorageSecretAzure:
audience = azureDefaultAudience
case lokiv1.ObjectStorageSecretGCS:
audience = opts.GCS.Audience
}
return corev1.Volume{
Name: saTokenVolumeName,
Expand Down
162 changes: 162 additions & 0 deletions operator/internal/manifests/storage/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,87 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage GCS with WorkloadIdentity",
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretGCS,
GCS: &GCSStorageConfig{
Audience: "test",
WorkloadIdentity: true,
},
},
dpl: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "loki-ingester",
},
},
},
},
},
},
want: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
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/gcp/serviceaccount",
},
},
Env: []corev1.EnvVar{
{
Name: EnvGoogleApplicationCredentials,
Value: "/etc/storage/secrets/key.json",
},
},
},
},
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: "test",
ExpirationSeconds: ptr.To[int64](3600),
Path: corev1.ServiceAccountTokenKey,
},
},
},
},
},
},
},
},
},
},
},
},
{
desc: "object storage S3",
opts: Options{
Expand Down Expand Up @@ -1009,6 +1090,87 @@ func TestConfigureStatefulSetForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage GCS with Workload Identity",
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretGCS,
GCS: &GCSStorageConfig{
Audience: "test",
WorkloadIdentity: true,
},
},
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/gcp/serviceaccount",
},
},
Env: []corev1.EnvVar{
{
Name: EnvGoogleApplicationCredentials,
Value: "/etc/storage/secrets/key.json",
},
},
},
},
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: "test",
ExpirationSeconds: ptr.To[int64](3600),
Path: corev1.ServiceAccountTokenKey,
},
},
},
},
},
},
},
},
},
},
},
},
{
desc: "object storage S3",
opts: Options{
Expand Down
4 changes: 3 additions & 1 deletion operator/internal/manifests/storage/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ type AzureStorageConfig struct {

// GCSStorageConfig for GCS storage config
type GCSStorageConfig struct {
Bucket string
Bucket string
Audience string
WorkloadIdentity bool
}

// S3StorageConfig for S3 storage config
Expand Down
4 changes: 4 additions & 0 deletions operator/internal/manifests/storage/var.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ const (
// KeyAzureEnvironmentName is the secret data key for the Azure cloud environment name.
KeyAzureEnvironmentName = "environment"

// KeyGCPWorkloadIdentityProviderAudience is the secret data key for the GCP Workload Identity Provider audience.
KeyGCPWorkloadIdentityProviderAudience = "audience"
// KeyGCPStorageBucketName is the secret data key for the GCS bucket name.
KeyGCPStorageBucketName = "bucketname"
// KeyGCPServiceAccountKeyFilename is the service account key filename containing the Google authentication credentials.
Expand Down Expand Up @@ -136,5 +138,7 @@ const (
azureDefaultAudience = "api://AzureADTokenExchange"
azureTokenVolumeDirectory = "/var/run/secrets/azure/serviceaccount"

gcpTokenVolumeDirectory = "/var/run/secrets/gcp/serviceaccount"

AnnotationCredentialsRequestsSecretRef = "loki.grafana.com/credentials-request-secret-ref"
)
Loading