Skip to content

Commit

Permalink
feat(operator): Add support for managed GCP WorkloadIdentity
Browse files Browse the repository at this point in the history
  • Loading branch information
periklis committed Nov 6, 2024
1 parent f5b0fb6 commit d004cde
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 26 deletions.
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.7.0
createdAt: "2024-10-30T09:43:17Z"
createdAt: "2024-11-06T10:07:13Z"
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: quay.io/openshift-logging/loki-operator:0.1.0
createdAt: "2024-10-30T09:43:19Z"
createdAt: "2024-11-06T10:07:15Z"
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.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
43 changes: 28 additions & 15 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 @@ -138,7 +138,7 @@ func extractSecrets(secretSpec lokiv1.ObjectStorageSecretSpec, objStore, tokenCC
case lokiv1.ObjectStorageSecretAzure:
storageOpts.Azure, err = extractAzureConfigSecret(objStore, credentialMode)
case lokiv1.ObjectStorageSecretGCS:
storageOpts.GCS, err = extractGCSConfigSecret(objStore, credentialMode)
storageOpts.GCS, err = extractGCSConfigSecret(objStore, tokenCCOAuth, credentialMode)
case lokiv1.ObjectStorageSecretS3:
storageOpts.S3, err = extractS3ConfigSecret(objStore, credentialMode)
case lokiv1.ObjectStorageSecretSwift:
Expand Down Expand Up @@ -182,7 +182,7 @@ func determineCredentialMode(spec lokiv1.ObjectStorageSecretSpec, secret *corev1
return lokiv1.CredentialModeToken, nil
}
case lokiv1.ObjectStorageSecretGCS:
_, credentialType, err := extractGoogleCredentialSource(secret)
_, credentialType, _, err := extractGoogleCredentialSource(secret, storage.KeyGCPServiceAccountKeyFilename)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -326,13 +326,14 @@ func validateBase64(data []byte) error {
return err
}

func extractGoogleCredentialSource(secret *corev1.Secret) (sourceFile, sourceType string, err error) {
keyJSON := secret.Data[storage.KeyGCPServiceAccountKeyFilename]
func extractGoogleCredentialSource(secret *corev1.Secret, key string) (sourceFile, sourceType, audience string, err error) {
keyJSON := secret.Data[key]
if len(keyJSON) == 0 {
return "", "", fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPServiceAccountKeyFilename)
return "", "", "", fmt.Errorf("%w: %s", errSecretMissingField, key)
}

credentialsFile := struct {
Audience string `json:"audience"`
CredentialsType string `json:"type"`
CredentialsSource struct {
File string `json:"file"`
Expand All @@ -341,20 +342,35 @@ func extractGoogleCredentialSource(secret *corev1.Secret) (sourceFile, sourceTyp

err = json.Unmarshal(keyJSON, &credentialsFile)
if err != nil {
return "", "", errGCPParseCredentialsFile
return "", "", "", errGCPParseCredentialsFile
}

return credentialsFile.CredentialsSource.File, credentialsFile.CredentialsType, nil
return credentialsFile.CredentialsSource.File, credentialsFile.CredentialsType, credentialsFile.Audience, nil
}

func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMode) (*storage.GCSStorageConfig, error) {
func extractGCSConfigSecret(s, tokenCCOAuth *corev1.Secret, credentialMode lokiv1.CredentialMode) (*storage.GCSStorageConfig, error) {
// Extract and validate mandatory fields
bucket := s.Data[storage.KeyGCPStorageBucketName]
if len(bucket) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPStorageBucketName)
}

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

_, _, audience, err := extractGoogleCredentialSource(tokenCCOAuth, storage.KeyGCPManagedServiceAccountKeyFilename)
if err != nil {
return nil, err
}

return &storage.GCSStorageConfig{
Audience: audience,
Bucket: string(bucket),
WorkloadIdentity: true,
}, nil
case lokiv1.CredentialModeStatic:
return &storage.GCSStorageConfig{
Bucket: string(bucket),
Expand All @@ -366,7 +382,7 @@ func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMo
}

// Check if correct credential source is used
credentialSource, _, err := extractGoogleCredentialSource(s)
credentialSource, _, _, err := extractGoogleCredentialSource(s, storage.KeyGCPServiceAccountKeyFilename)
if err != nil {
return nil, err
}
Expand All @@ -380,12 +396,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
14 changes: 11 additions & 3 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
Loading

0 comments on commit d004cde

Please sign in to comment.