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

pull/14842/head
Periklis Tsirakidis 1 year ago committed by GitHub
parent 90c5dbf0d3
commit 7635a5cffa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      operator/bundle/community-openshift/manifests/loki-operator.clusterserviceversion.yaml
  2. 2
      operator/bundle/openshift/manifests/loki-operator.clusterserviceversion.yaml
  3. 2
      operator/config/manifests/community-openshift/bases/loki-operator.clusterserviceversion.yaml
  4. 2
      operator/config/manifests/openshift/bases/loki-operator.clusterserviceversion.yaml
  5. 31
      operator/internal/config/managed_auth.go
  6. 18
      operator/internal/handlers/internal/storage/secrets.go
  7. 43
      operator/internal/handlers/internal/storage/secrets_test.go
  8. 9
      operator/internal/manifests/openshift/credentialsrequest.go
  9. 19
      operator/internal/manifests/storage/configure.go
  10. 97
      operator/internal/manifests/storage/configure_test.go
  11. 6
      operator/internal/manifests/storage/var.go

@ -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

@ -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

@ -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:

@ -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

@ -1,6 +1,9 @@
package config
import "os"
import (
"fmt"
"os"
)
type AWSEnvironment struct {
RoleARN string
@ -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 {
@ -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{
@ -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

@ -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)")
@ -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,
@ -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),
@ -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) {

@ -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
}
@ -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) {
@ -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)

@ -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())

@ -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)
}
@ -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{}
@ -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,

@ -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{

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

Loading…
Cancel
Save