Skip to content

Commit

Permalink
feat(operator): Add support for managed GCP WorkloadIdentity (grafana…
Browse files Browse the repository at this point in the history
  • Loading branch information
periklis committed Nov 18, 2024
1 parent 7bff32d commit 49da9ad
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 21 deletions.
4 changes: 4 additions & 0 deletions operator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Main

## Release 6.0.3

- [14752](https://github.com/grafana/loki/pull/14752) **periklis**: Add support for managed GCP WorkloadIdentity

## Release 6.0.2

- [14613](https://github.com/grafana/loki/pull/14613) **xperimental**: fix(operator): Disable log level discovery for OpenShift tenancy modes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ metadata:
categories: OpenShift Optional, Logging & Tracing
certified: "false"
containerImage: docker.io/grafana/loki-operator:0.6.1
createdAt: "2024-10-23T18:24:03Z"
createdAt: "2024-11-18T12:58:47Z"
description: The Community Loki Operator provides Kubernetes native deployment
and management of Loki and related logging components.
features.operators.openshift.io/disconnected: "true"
Expand All @@ -159,7 +159,7 @@ metadata:
features.operators.openshift.io/tls-profiles: "true"
features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "true"
features.operators.openshift.io/token-auth-gcp: "false"
features.operators.openshift.io/token-auth-gcp: "true"
operators.operatorframework.io/builder: operator-sdk-unknown
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
repository: https://github.com/grafana/loki/tree/main/operator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ metadata:
categories: OpenShift Optional, Logging & Tracing
certified: "false"
containerImage: docker.io/grafana/loki-operator:0.6.1
createdAt: "2024-10-23T18:24:01Z"
createdAt: "2024-11-18T12:58:45Z"
description: The Community Loki Operator provides Kubernetes native deployment
and management of Loki and related logging components.
operators.operatorframework.io/builder: operator-sdk-unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ metadata:
categories: OpenShift Optional, Logging & Tracing
certified: "false"
containerImage: quay.io/openshift-logging/loki-operator:0.1.0
createdAt: "2024-10-23T18:24:05Z"
createdAt: "2024-11-18T12:58:49Z"
description: |
The Loki Operator for OCP provides a means for configuring and managing a Loki stack for cluster logging.
## Prerequisites and Requirements
Expand All @@ -166,7 +166,7 @@ metadata:
features.operators.openshift.io/tls-profiles: "true"
features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "true"
features.operators.openshift.io/token-auth-gcp: "false"
features.operators.openshift.io/token-auth-gcp: "true"
olm.skipRange: '>=5.8.0-0 <6.0.0'
operatorframework.io/cluster-monitoring: "true"
operatorframework.io/suggested-namespace: openshift-operators-redhat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ metadata:
features.operators.openshift.io/tls-profiles: "true"
features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "true"
features.operators.openshift.io/token-auth-gcp: "false"
features.operators.openshift.io/token-auth-gcp: "true"
repository: https://github.com/grafana/loki/tree/main/operator
support: Grafana Loki SIG Operator
labels:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ metadata:
features.operators.openshift.io/tls-profiles: "true"
features.operators.openshift.io/token-auth-aws: "true"
features.operators.openshift.io/token-auth-azure: "true"
features.operators.openshift.io/token-auth-gcp: "false"
features.operators.openshift.io/token-auth-gcp: "true"
olm.skipRange: '>=5.8.0-0 <6.0.0'
operatorframework.io/cluster-monitoring: "true"
operatorframework.io/suggested-namespace: openshift-operators-redhat
Expand Down
31 changes: 30 additions & 1 deletion operator/internal/config/managed_auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package config

import "os"
import (
"fmt"
"os"
)

type AWSEnvironment struct {
RoleARN string
Expand All @@ -13,9 +16,15 @@ type AzureEnvironment struct {
Region string
}

type GCPEnvironment struct {
Audience string
ServiceAccountEmail string
}

type TokenCCOAuthConfig struct {
AWS *AWSEnvironment
Azure *AzureEnvironment
GCP *GCPEnvironment
}

func discoverTokenCCOAuthConfig() *TokenCCOAuthConfig {
Expand All @@ -28,6 +37,12 @@ func discoverTokenCCOAuthConfig() *TokenCCOAuthConfig {
subscriptionID := os.Getenv("SUBSCRIPTIONID")
region := os.Getenv("REGION")

// GCP
projectNumber := os.Getenv("PROJECT_NUMBER")
poolID := os.Getenv("POOL_ID")
providerID := os.Getenv("PROVIDER_ID")
serviceAccountEmail := os.Getenv("SERVICE_ACCOUNT_EMAIL")

switch {
case roleARN != "":
return &TokenCCOAuthConfig{
Expand All @@ -44,6 +59,20 @@ func discoverTokenCCOAuthConfig() *TokenCCOAuthConfig {
Region: region,
},
}
case projectNumber != "" && poolID != "" && providerID != "" && serviceAccountEmail != "":
audience := fmt.Sprintf(
"//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s",
projectNumber,
poolID,
providerID,
)

return &TokenCCOAuthConfig{
GCP: &GCPEnvironment{
Audience: audience,
ServiceAccountEmail: serviceAccountEmail,
},
}
}

return nil
Expand Down
18 changes: 12 additions & 6 deletions operator/internal/handlers/internal/storage/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ var (
errSecretUnknownSSEType = errors.New("unsupported SSE type (supported: SSE-KMS, SSE-S3)")
errSecretHashError = errors.New("error calculating hash for secret")

errSecretUnknownCredentialMode = errors.New("unknown credential mode")
errSecretUnsupportedCredentialMode = errors.New("combination of storage type and credential mode not supported")
errSecretUnknownCredentialMode = errors.New("unknown credential mode")

errAzureManagedIdentityNoOverride = errors.New("when in managed mode, storage secret can not contain credentials")
errAzureInvalidEnvironment = errors.New("azure environment invalid (valid values: AzureGlobal, AzureChinaCloud, AzureGermanCloud, AzureUSGovernment)")
Expand All @@ -47,6 +46,7 @@ var (

errGCPParseCredentialsFile = errors.New("gcp storage secret cannot be parsed from JSON content")
errGCPWrongCredentialSourceFile = errors.New("credential source in secret needs to point to token file")
errGCPInvalidCredentialsFile = errors.New("gcp credentials file contains invalid fields")

azureValidEnvironments = map[string]bool{
"AzureGlobal": true,
Expand Down Expand Up @@ -355,6 +355,15 @@ func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMo
}

switch credentialMode {
case lokiv1.CredentialModeTokenCCO:
if _, ok := s.Data[storage.KeyGCPServiceAccountKeyFilename]; ok {
return nil, fmt.Errorf("%w: %s", errGCPInvalidCredentialsFile, "key.json must not be set for CredentialModeTokenCCO")
}

return &storage.GCSStorageConfig{
Bucket: string(bucket),
WorkloadIdentity: true,
}, nil
case lokiv1.CredentialModeStatic:
return &storage.GCSStorageConfig{
Bucket: string(bucket),
Expand All @@ -380,12 +389,9 @@ func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMo
WorkloadIdentity: true,
Audience: audience,
}, nil
case lokiv1.CredentialModeTokenCCO:
return nil, fmt.Errorf("%w: type: %s credentialMode: %s", errSecretUnsupportedCredentialMode, lokiv1.ObjectStorageSecretGCS, credentialMode)
default:
return nil, fmt.Errorf("%w: %s", errSecretUnknownCredentialMode, credentialMode)
}

return nil, fmt.Errorf("%w: %s", errSecretUnknownCredentialMode, credentialMode)
}

func extractS3ConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMode) (*storage.S3StorageConfig, error) {
Expand Down
43 changes: 42 additions & 1 deletion operator/internal/handlers/internal/storage/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ func TestGCSExtract(t *testing.T) {
type test struct {
name string
secret *corev1.Secret
tokenAuth *corev1.Secret
featureGates configv1.FeatureGates
wantError string
wantCredentialMode lokiv1.CredentialMode
}
Expand Down Expand Up @@ -343,6 +345,45 @@ func TestGCSExtract(t *testing.T) {
},
wantCredentialMode: lokiv1.CredentialModeToken,
},
{
name: "invalid for token CCO",
featureGates: configv1.FeatureGates{
OpenShift: configv1.OpenShiftFeatureGates{
Enabled: true,
TokenCCOAuthEnv: true,
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"key.json": []byte("{\"type\": \"external_account\", \"audience\": \"\", \"service_account_id\": \"\"}"),
},
},
wantError: "gcp credentials file contains invalid fields: key.json must not be set for CredentialModeTokenCCO",
},
{
name: "valid for token CCO",
featureGates: configv1.FeatureGates{
OpenShift: configv1.OpenShiftFeatureGates{
Enabled: true,
TokenCCOAuthEnv: true,
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
},
},
tokenAuth: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "token-auth-config"},
Data: map[string][]byte{
"service_account.json": []byte("{\"type\": \"external_account\", \"audience\": \"test\", \"service_account_id\": \"\"}"),
},
},
wantCredentialMode: lokiv1.CredentialModeTokenCCO,
},
}
for _, tst := range table {
t.Run(tst.name, func(t *testing.T) {
Expand All @@ -352,7 +393,7 @@ func TestGCSExtract(t *testing.T) {
Type: lokiv1.ObjectStorageSecretGCS,
}

opts, err := extractSecrets(spec, tst.secret, nil, configv1.FeatureGates{})
opts, err := extractSecrets(spec, tst.secret, tst.tokenAuth, tst.featureGates)
if tst.wantError == "" {
require.NoError(t, err)
require.Equal(t, tst.wantCredentialMode, opts.CredentialMode)
Expand Down
9 changes: 9 additions & 0 deletions operator/internal/manifests/openshift/credentialsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ func encodeProviderSpec(env *config.TokenCCOAuthConfig) (*runtime.RawExtension,
AzureSubscriptionID: azure.SubscriptionID,
AzureTenantID: azure.TenantID,
}
case env.GCP != nil:
spec = &cloudcredentialv1.GCPProviderSpec{
PredefinedRoles: []string{
"roles/iam.workloadIdentityUser",
"roles/storage.objectAdmin",
},
Audience: env.GCP.Audience,
ServiceAccountEmail: env.GCP.ServiceAccountEmail,
}
}

encodedSpec, err := cloudcredentialv1.Codec.EncodeProviderSpec(spec.DeepCopyObject())
Expand Down
19 changes: 15 additions & 4 deletions operator/internal/manifests/storage/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ func ensureObjectStoreCredentials(p *corev1.PodSpec, opts Options) corev1.PodSpe
volumes = append(volumes, saTokenVolume(opts))
container.VolumeMounts = append(container.VolumeMounts, saTokenVolumeMount)

if opts.OpenShift.TokenCCOAuthEnabled() && opts.S3 != nil && opts.S3.STS {
isSTS := opts.S3 != nil && opts.S3.STS
isWIF := opts.GCS != nil && opts.GCS.WorkloadIdentity
if opts.OpenShift.TokenCCOAuthEnabled() && (isSTS || isWIF) {
volumes = append(volumes, tokenCCOAuthConfigVolume(opts))
container.VolumeMounts = append(container.VolumeMounts, tokenCCOAuthConfigVolumeMount)
}
Expand Down Expand Up @@ -223,8 +225,14 @@ func tokenAuthCredentials(opts Options) []corev1.EnvVar {
envVarFromValue(EnvAzureFederatedTokenFile, ServiceAccountTokenFilePath),
}
case lokiv1.ObjectStorageSecretGCS:
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(secretDirectory, KeyGCPServiceAccountKeyFilename)),
if opts.OpenShift.TokenCCOAuthEnabled() {
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(tokenAuthConfigDirectory, KeyGCPManagedServiceAccountKeyFilename)),
}
} else {
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(secretDirectory, KeyGCPServiceAccountKeyFilename)),
}
}
default:
return []corev1.EnvVar{}
Expand Down Expand Up @@ -326,7 +334,10 @@ func saTokenVolume(opts Options) corev1.Volume {
audience = opts.Azure.Audience
}
case lokiv1.ObjectStorageSecretGCS:
audience = opts.GCS.Audience
audience = gcpDefaultAudience
if opts.GCS.Audience != "" {
audience = opts.GCS.Audience
}
}
return corev1.Volume{
Name: saTokenVolumeName,
Expand Down
Loading

0 comments on commit 49da9ad

Please sign in to comment.