Skip to content

Commit

Permalink
feat(operator): Add support for managed GCP WorkloadIdentity (#14752)
Browse files Browse the repository at this point in the history
  • Loading branch information
periklis authored Nov 8, 2024
1 parent 90c5dbf commit 7635a5c
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -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.9.0-0 <6.1.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.9.0-0 <6.1.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
97 changes: 97 additions & 0 deletions operator/internal/manifests/storage/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,103 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage GCS with Workload Identity and OpenShift Managed Credentials",
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretGCS,
CredentialMode: lokiv1.CredentialModeTokenCCO,
GCS: &GCSStorageConfig{
WorkloadIdentity: true,
},
OpenShift: OpenShiftOptions{
Enabled: true,
CloudCredentials: CloudCredentials{
SecretName: "cloud-credentials",
SHA1: "deadbeef",
},
},
},
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: saTokenVolumeMountPath,
},
tokenCCOAuthConfigVolumeMount,
},
Env: []corev1.EnvVar{
{
Name: EnvGoogleApplicationCredentials,
Value: "/etc/storage/token-auth/service_account.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: gcpDefaultAudience,
ExpirationSeconds: ptr.To[int64](3600),
Path: corev1.ServiceAccountTokenKey,
},
},
},
},
},
},
{
Name: tokenAuthConfigVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "cloud-credentials",
},
},
},
},
},
},
},
},
},
{
desc: "object storage S3",
opts: Options{
Expand Down
6 changes: 4 additions & 2 deletions operator/internal/manifests/storage/var.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const (
KeyGCPStorageBucketName = "bucketname"
// KeyGCPServiceAccountKeyFilename is the service account key filename containing the Google authentication credentials.
KeyGCPServiceAccountKeyFilename = "key.json"
// KeyGCPManagedServiceAccountKeyFilename is the service account key filename for the managed Google service account.
KeyGCPManagedServiceAccountKeyFilename = "service_account.json"

// KeySwiftAuthURL is the secret data key for the OpenStack Swift authentication URL.
KeySwiftAuthURL = "auth_url"
Expand Down Expand Up @@ -140,9 +142,9 @@ const (
tokenAuthConfigVolumeName = "token-auth-config"
tokenAuthConfigDirectory = "/etc/storage/token-auth"

awsDefaultAudience = "sts.amazonaws.com"

awsDefaultAudience = "sts.amazonaws.com"
azureDefaultAudience = "api://AzureADTokenExchange"
gcpDefaultAudience = "openshift"

azureManagedCredentialKeyClientID = "azure_client_id"
azureManagedCredentialKeyTenantID = "azure_tenant_id"
Expand Down

0 comments on commit 7635a5c

Please sign in to comment.