Skip to content

Commit

Permalink
operator: Add support for running with Azure Workload Identity (grafa…
Browse files Browse the repository at this point in the history
  • Loading branch information
xperimental authored Feb 1, 2024
1 parent 098eef7 commit 009c53a
Showing 8 changed files with 272 additions and 21 deletions.
1 change: 1 addition & 0 deletions operator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Main

- [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
- [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
56 changes: 46 additions & 10 deletions operator/internal/handlers/internal/storage/secrets.go
Original file line number Diff line number Diff line change
@@ -28,6 +28,9 @@ var (
errSecretHashError = errors.New("error calculating hash for secret")

errS3NoAuth = errors.New("missing secret fields for static or sts authentication")

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")
)

func getSecrets(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (*corev1.Secret, *corev1.Secret, error) {
@@ -165,25 +168,58 @@ func extractAzureConfigSecret(s *corev1.Secret) (*storage.AzureStorageConfig, er
if len(container) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAzureStorageContainerName)
}
name := s.Data[storage.KeyAzureStorageAccountName]
if len(name) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAzureStorageAccountName)
}
key := s.Data[storage.KeyAzureStorageAccountKey]
if len(key) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAzureStorageAccountKey)
workloadIdentity, err := validateAzureCredentials(s)
if err != nil {
return nil, err
}

// Extract and validate optional fields
endpointSuffix := s.Data[storage.KeyAzureStorageEndpointSuffix]

return &storage.AzureStorageConfig{
Env: string(env),
Container: string(container),
EndpointSuffix: string(endpointSuffix),
Env: string(env),
Container: string(container),
EndpointSuffix: string(endpointSuffix),
WorkloadIdentity: workloadIdentity,
}, nil
}

func validateAzureCredentials(s *corev1.Secret) (workloadIdentity bool, err error) {
accountName := s.Data[storage.KeyAzureStorageAccountName]
accountKey := s.Data[storage.KeyAzureStorageAccountKey]
clientID := s.Data[storage.KeyAzureStorageClientID]
tenantID := s.Data[storage.KeyAzureStorageTenantID]
subscriptionID := s.Data[storage.KeyAzureStorageSubscriptionID]

if len(accountName) == 0 {
return false, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAzureStorageAccountName)
}

if len(accountKey) == 0 && len(clientID) == 0 {
return false, errAzureNoCredentials
}

if len(accountKey) > 0 && len(clientID) > 0 {
return false, errAzureMixedCredentials
}

if len(accountKey) > 0 {
// have both account_name and account_key -> no workload identity federation
return false, nil
}

// assume workload-identity from here on
if len(tenantID) == 0 {
return false, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAzureStorageTenantID)
}

if len(subscriptionID) == 0 {
return false, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAzureStorageSubscriptionID)
}

return true, nil
}

func extractGCSConfigSecret(s *corev1.Secret) (*storage.GCSStorageConfig, error) {
// Extract and validate mandatory fields
bucket := s.Data[storage.KeyGCPStorageBucketName]
62 changes: 59 additions & 3 deletions operator/internal/handlers/internal/storage/secrets_test.go
Original file line number Diff line number Diff line change
@@ -101,7 +101,7 @@ func TestAzureExtract(t *testing.T) {
wantError: "missing secret field: account_name",
},
{
name: "missing account_key",
name: "no account_key or client_id",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
@@ -110,10 +110,51 @@ func TestAzureExtract(t *testing.T) {
"account_name": []byte("id"),
},
},
wantError: "missing secret field: account_key",
wantError: errAzureNoCredentials.Error(),
},
{
name: "all mandatory set",
name: "both account_key and client_id set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"environment": []byte("here"),
"container": []byte("this,that"),
"account_name": []byte("test-account-name"),
"account_key": []byte("test-account-key"),
"client_id": []byte("test-client-id"),
},
},
wantError: errAzureMixedCredentials.Error(),
},
{
name: "missing tenant_id",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"environment": []byte("here"),
"container": []byte("this,that"),
"account_name": []byte("test-account-name"),
"client_id": []byte("test-client-id"),
},
},
wantError: "missing secret field: tenant_id",
},
{
name: "missing subscription_id",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"environment": []byte("here"),
"container": []byte("this,that"),
"account_name": []byte("test-account-name"),
"client_id": []byte("test-client-id"),
"tenant_id": []byte("test-tenant-id"),
},
},
wantError: "missing secret field: subscription_id",
},
{
name: "mandatory for normal authentication set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
@@ -124,6 +165,21 @@ func TestAzureExtract(t *testing.T) {
},
},
},
{
name: "mandatory for workload-identity set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"environment": []byte("here"),
"container": []byte("this,that"),
"account_name": []byte("test-account-name"),
"client_id": []byte("test-client-id"),
"tenant_id": []byte("test-tenant-id"),
"subscription_id": []byte("test-subscription-id"),
"region": []byte("test-region"),
},
},
},
{
name: "all set including optional",
secret: &corev1.Secret{
4 changes: 4 additions & 0 deletions operator/internal/manifests/internal/config/loki-config.yaml
Original file line number Diff line number Diff line change
@@ -13,7 +13,11 @@ common:
environment: {{ .Env }}
container_name: {{ .Container }}
account_name: ${AZURE_STORAGE_ACCOUNT_NAME}
{{- if .WorkloadIdentity }}
use_federated_token: true
{{- else }}
account_key: ${AZURE_STORAGE_ACCOUNT_KEY}
{{- end }}
{{- with .EndpointSuffix }}
endpoint_suffix: {{ . }}
{{- end }}
16 changes: 14 additions & 2 deletions operator/internal/manifests/storage/configure.go
Original file line number Diff line number Diff line change
@@ -56,7 +56,6 @@ func ConfigureStatefulSet(d *appsv1.StatefulSet, opts Options) error {
// With this, the deployment will expose credentials specific environment variables.
func configureDeployment(d *appsv1.Deployment, opts Options) error {
p := ensureObjectStoreCredentials(&d.Spec.Template.Spec, opts)

if err := mergo.Merge(&d.Spec.Template.Spec, p, mergo.WithOverride); err != nil {
return kverrors.Wrap(err, "failed to merge gcs object storage spec ")
}
@@ -83,7 +82,6 @@ func configureDeploymentCA(d *appsv1.Deployment, tls *TLSConfig) error {
// With this, the statefulset will expose credentials specific environment variable.
func configureStatefulSet(s *appsv1.StatefulSet, opts Options) error {
p := ensureObjectStoreCredentials(&s.Spec.Template.Spec, opts)

if err := mergo.Merge(&s.Spec.Template.Spec, p, mergo.WithOverride); err != nil {
return kverrors.Wrap(err, "failed to merge gcs object storage spec ")
}
@@ -195,6 +193,14 @@ func managedAuthCredentials(opts Options) []corev1.EnvVar {
envVarFromValue(EnvAWSWebIdentityTokenFile, path.Join(opts.S3.WebIdentityTokenFile, "token")),
}
}
case lokiv1.ObjectStorageSecretAzure:
return []corev1.EnvVar{
envVarFromSecret(EnvAzureStorageAccountName, opts.SecretName, KeyAzureStorageAccountName),
envVarFromSecret(EnvAzureClientID, opts.SecretName, KeyAzureStorageClientID),
envVarFromSecret(EnvAzureTenantID, opts.SecretName, KeyAzureStorageTenantID),
envVarFromSecret(EnvAzureSubscriptionID, opts.SecretName, KeyAzureStorageSubscriptionID),
envVarFromValue(EnvAzureFederatedTokenFile, path.Join(azureTokenVolumeDirectory, "token")),
}
default:
return []corev1.EnvVar{}
}
@@ -273,6 +279,8 @@ func managedAuthEnabled(opts Options) bool {
switch opts.SharedStore {
case lokiv1.ObjectStorageSecretS3:
return opts.S3 != nil && opts.S3.STS
case lokiv1.ObjectStorageSecretAzure:
return opts.Azure != nil && opts.Azure.WorkloadIdentity
default:
return false
}
@@ -293,6 +301,8 @@ func saTokenVolumeMount(opts Options) corev1.VolumeMount {
switch opts.SharedStore {
case lokiv1.ObjectStorageSecretS3:
tokenPath = opts.S3.WebIdentityTokenFile
case lokiv1.ObjectStorageSecretAzure:
tokenPath = azureTokenVolumeDirectory
}
return corev1.VolumeMount{
Name: saTokenVolumeName,
@@ -312,6 +322,8 @@ func saTokenVolume(opts Options) corev1.Volume {
if opts.OpenShift.Enabled {
audience = AWSOpenShiftAudience
}
case lokiv1.ObjectStorageSecretAzure:
audience = azureDefaultAudience
}
return corev1.Volume{
Name: saTokenVolumeName,
124 changes: 124 additions & 0 deletions operator/internal/manifests/storage/configure_test.go
Original file line number Diff line number Diff line change
@@ -168,6 +168,130 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage Azure with WIF",
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretAzure,
Azure: &AzureStorageConfig{
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/azure/serviceaccount",
},
},
Env: []corev1.EnvVar{
{
Name: EnvAzureStorageAccountName,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test",
},
Key: KeyAzureStorageAccountName,
},
},
},
{
Name: EnvAzureClientID,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test",
},
Key: KeyAzureStorageClientID,
},
},
},
{
Name: EnvAzureTenantID,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test",
},
Key: KeyAzureStorageTenantID,
},
},
},
{
Name: EnvAzureSubscriptionID,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test",
},
Key: KeyAzureStorageSubscriptionID,
},
},
},
{
Name: EnvAzureFederatedTokenFile,
Value: "/var/run/secrets/azure/serviceaccount/token",
},
},
},
},
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: azureDefaultAudience,
ExpirationSeconds: ptr.To[int64](3600),
Path: corev1.ServiceAccountTokenKey,
},
},
},
},
},
},
},
},
},
},
},
},
{
desc: "object storage GCS",
opts: Options{
7 changes: 4 additions & 3 deletions operator/internal/manifests/storage/options.go
Original file line number Diff line number Diff line change
@@ -25,9 +25,10 @@ type Options struct {

// AzureStorageConfig for Azure storage config
type AzureStorageConfig struct {
Env string
Container string
EndpointSuffix string
Env string
Container string
EndpointSuffix string
WorkloadIdentity bool
}

// GCSStorageConfig for GCS storage config
Loading

0 comments on commit 009c53a

Please sign in to comment.