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

feat(operator): Add support for managed GCP WorkloadIdentity #14752

Merged
merged 4 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading
Loading