Skip to content

feat: Provider v2#5527

Draft
moolen wants to merge 15 commits intomainfrom
mj-v2-poc
Draft

feat: Provider v2#5527
moolen wants to merge 15 commits intomainfrom
mj-v2-poc

Conversation

@moolen
Copy link
Copy Markdown
Member

@moolen moolen commented Oct 30, 2025

Summary

Overview

The v2 provider enables out-of-process providers using gRPC, allowing a single provider codebase to expose multiple APIs (e.g., AWS SecretsManager, ParameterStore, ECR, STS) without requiring modifications to existing v1 provider implementations.

Open Issues

  • figure out options for mTLS authentication
  • research/document alternatives and their trade offs (such as: autodiscovery, referencing provider CRs directly)
  • referent authentication
  • provider-specific cmd flags (in compatibility with v1 providers)
    • favour a ENV-based approach for provider specific features. Some of the features could/should be part of the CRD.
  • gRPC client instantiation & scalability & testing strategy
  • TBD

ADR Deliverables

  • Controller Flow Diagram v1 vs v2
    • Adapter: why & how
    • Provider v2 internals
    • autogen of main.go
  • client_manager & gRPC connection pooling
  • mTLS, autodiscovery & credential management, threat model & trust boundaries
  • deployment of provider
  • command line flags & provider configuration
  • testing strategy
eso-secretstore-v2-SecretStorev2_Provider drawio (2)

Flow Diagram

graph TB
    subgraph "ESO Controller (In-Process)"
        A[ExternalSecret Controller] -->|"GetProviderSecretData()"| B[Client Manager]
        B -->|"Check storeRef.kind == Provider"| C{Is v2 Provider?}
        C -->|Yes| D[Create gRPC Client]
        C -->|No| E[Use v1 Provider In-Process]
        D --> F[V2ClientWrapper<br/>v1→v2 Adapter]
        F -->|"Implements<br/>esv1.SecretsClient"| G[gRPC Client]
    end
    
    subgraph "gRPC Communication"
        G -->|"GetSecretRequest<br/>{ProviderRef, RemoteRef}"| H[mTLS Connection]
    end
    
    subgraph "Provider Server (Out-of-Process)"
        H --> I[AdapterServer<br/>v2→v1 Adapter]
        I -->|"1. Parse ProviderRef<br/>(apiVersion + kind)"| J{Resolve GVK}
        J -->|"SecretsManager"| K[AWS v1 Provider]
        J -->|"ParameterStore"| K
        J -->|"ECRAuthToken"| K
        J -->|"STSSessionToken"| K
        K -->|"2. Fetch CR<br/>(e.g., SecretsManager)"| M[v1.SyntheticStore]
        M -->|"4. Call v1 provider"| N[provider.NewClient]
        N -->|"5. Get secret"| O[AWS API]
        O -->|"6. Return secret data"| H
    end

    style A fill:#e1f5ff
    style F fill:#ffe1f5
    style I fill:#fff5e1
    style K fill:#e1ffe1
Loading

1. Client-Side: v2 → v1 Adapter (In-Process)

How ExternalSecret Controller Uses gRPC Clients

In externalsecret_controller_secret.go, the reconciler uses the Client Manager to obtain provider clients:

// We MUST NOT create multiple instances of a provider client (mostly due to limitations with GCP)
// Clientmanager keeps track of the client instances
// that are created during the fetching process and closes clients
// if needed.
mgr := secretstore.NewManager(r.Client, r.ControllerClass, r.EnableFloodGate)

Client Manager: Creating gRPC Clients

When a SecretStoreRef has kind: Provider, the manager creates a gRPC client:

// Get returns a provider client from the given storeRef or sourceRef.secretStoreRef
// while sourceRef.SecretStoreRef takes precedence over storeRef.
// Do not close the client returned from this func, instead close
// the manager once you're done with recinciling the external secret.
func (m *Manager) Get(ctx context.Context, storeRef esv1.SecretStoreRef, namespace string, sourceRef *esv1.StoreGeneratorSourceRef) (esv1.SecretsClient, error) {
	if storeRef.Kind == "Provider" {
		return m.getV2ProviderClient(ctx, storeRef.Name, namespace)
	}
	if sourceRef != nil && sourceRef.SecretStoreRef != nil {
		storeRef = *sourceRef.SecretStoreRef
	}
	store, err := m.getStore(ctx, &storeRef, namespace)
	if err != nil {
		return nil, err
	}
	// check if store should be handled by this controller instance
	if !ShouldProcessStore(store, m.controllerClass) {
		return nil, errors.New("can not reference unmanaged store")
	}
	// when using ClusterSecretStore, validate the ClusterSecretStore namespace conditions
	shouldProcess, err := m.shouldProcessSecret(store, namespace)
	if err != nil {
		return nil, err
	}
	if !shouldProcess {
		return nil, fmt.Errorf(errClusterStoreMismatch, store.GetName(), namespace)
	}

	if m.enableFloodgate {
		err := assertStoreIsUsable(store)
		if err != nil {
			return nil, err
		}
	}
	return m.GetFromStore(ctx, store, namespace)
}

The getV2ProviderClient method:

  1. Fetches the Provider resource
  2. Creates a gRPC connection with TLS
  3. Wraps it with V2ClientWrapper (the v2→v1 adapter)
// Create gRPC client
grpcClient, err := grpc.NewClient(address, tlsConfig)
if err != nil {
	return nil, fmt.Errorf("failed to create gRPC client for Provider %q: %w", providerName, err)
}

// Convert ProviderReference to protobuf format
providerRef := &pb.ProviderReference{
	ApiVersion: provider.Spec.Config.ProviderRef.APIVersion,
	Kind:       provider.Spec.Config.ProviderRef.Kind,
	Name:       provider.Spec.Config.ProviderRef.Name,
	Namespace:  provider.Spec.Config.ProviderRef.Namespace,
}

// Wrap with V2ClientWrapper
wrappedClient := adapter.NewV2ClientWrapper(grpcClient, providerRef, namespace)

V2ClientWrapper: Implementing v1.SecretsClient

The wrapper adapts the gRPC v2.Provider interface to the v1 SecretsClient interface:

// V2ClientWrapper wraps a v2.Provider (gRPC client) and exposes it as an esv1.SecretsClient.
// This allows v2 providers to be used with the existing client manager infrastructure.
type V2ClientWrapper struct {
	v2Provider      v2.Provider
	providerRef     *pb.ProviderReference
	sourceNamespace string
}

// Ensure V2ClientWrapper implements SecretsClient interface
var _ esv1.SecretsClient = &V2ClientWrapper{}

// NewV2ClientWrapper creates a new wrapper that adapts a v2.Provider to esv1.SecretsClient.
func NewV2ClientWrapper(v2Provider v2.Provider, providerRef *pb.ProviderReference, sourceNamespace string) esv1.SecretsClient {
	return &V2ClientWrapper{
		v2Provider:      v2Provider,
		providerRef:     providerRef,
		sourceNamespace: sourceNamespace,
	}
}

// GetSecret retrieves a single secret from the provider.
func (w *V2ClientWrapper) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
	return w.v2Provider.GetSecret(ctx, ref, w.providerRef, w.sourceNamespace)
}

gRPC Client: Making RPC Calls

The gRPC client converts v1 types to protobuf and makes RPC calls:

// GetSecret retrieves a single secret from the provider via gRPC.
func (c *grpcProviderClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef, providerRef *pb.ProviderReference, sourceNamespace string) ([]byte, error) {
	c.log.V(1).Info("getting secret via gRPC",
		"key", ref.Key,
		"version", ref.Version,
		"property", ref.Property,
		"connectionState", c.conn.GetState().String(),
		"providerRef", providerRef,
		"sourceNamespace", sourceNamespace)

	// Check connection state before call
	state := c.conn.GetState()
	if state != connectivity.Ready && state != connectivity.Idle {
		c.log.Info("connection not ready, attempting to reconnect",
			"state", state.String(),
			"target", c.conn.Target())
	}

	// Create context with timeout
	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
	defer cancel()

	// Convert v1 reference to protobuf message
	pbRef := &pb.ExternalSecretDataRemoteRef{
		Key:              ref.Key,
		Version:          ref.Version,
		Property:         ref.Property,
		DecodingStrategy: string(ref.DecodingStrategy),
		MetadataPolicy:   string(ref.MetadataPolicy),
	}

	// Make gRPC call with provider reference
	req := &pb.GetSecretRequest{
		RemoteRef:       pbRef,
		ProviderRef:     providerRef,
		SourceNamespace: sourceNamespace,
	}

	c.log.V(1).Info("calling GetSecret RPC",
		"target", c.conn.Target(),
		"timeout", defaultTimeout.String())

	resp, err := c.client.GetSecret(ctx, req)
	if err != nil {
		c.log.Error(err, "GetSecret RPC failed",
			"key", ref.Key,
			"connectionState", c.conn.GetState().String(),
			"target", c.conn.Target())
		return nil, fmt.Errorf("failed to get secret via gRPC: %w", err)
	}

	c.log.V(1).Info("GetSecret RPC succeeded",
		"key", ref.Key,
		"valueLength", len(resp.Value))

	return resp.Value, nil
}

2. Multiple APIs via ProviderReference Mapping

Separate CRDs for Each AWS Service

The AWS v2 provider exposes separate Kubernetes Custom Resources for different services:

var (
	// GroupVersion is group version used to register these objects
	GroupVersion = schema.GroupVersion{Group: "provider.external-secrets.io", Version: "v2alpha1"}

	// SecretsManagerKind is the kind name used for SecretsManager resources.
	SecretsManagerKind = "SecretsManager"

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)

Example: SecretsManager CRD:

// SecretsManager is the Schema for AWS Secrets Manager provider configuration
type SecretsManager struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   SecretsManagerSpec   `json:"spec,omitempty"`
	Status SecretsManagerStatus `json:"status,omitempty"`
}

Future expansion will include ParameterStore, ECRAuthToken, STSSessionToken, etc., all served by the same gRPC server process.


3. Server-Side: v1 → v2 Adapter (Out-of-Process)

AdapterServer: Mapping ProviderRef to v1 Clients

The gRPC server uses AdapterServer to map incoming ProviderReference (apiVersion + kind) to v1 provider implementations:

// AdapterServer wraps a v1 provider and exposes it as a v2 gRPC service.
// This allows existing v1 provider implementations to be used in the v2 architecture.
type AdapterServer struct {
	pb.UnimplementedSecretStoreProviderServer
	kubeClient client.Client

	// we support multiple v1 providers, so we need to map the v2 provider
	// with apiVersion+kind to the corresponding v1 provider
	resourceMapping ProviderMapping
	specMapper      SpecMapper
}

type ProviderMapping map[schema.GroupVersionKind]esv1.ProviderInterface

// maps a provider reference to a SecretStoreSpec
// which is used to create a synthetic store for the v1 provider.
type SpecMapper func(ref *pb.ProviderReference) (*esv1.SecretStoreSpec, error)

// NewAdapterServer creates a new AdapterServer that wraps a v1 provider.
func NewAdapterServer(kubeClient client.Client, resourceMapping ProviderMapping, specMapping SpecMapper) *AdapterServer {
	return &AdapterServer{
		kubeClient:      kubeClient,
		resourceMapping: resourceMapping,
		specMapper:      specMapping,
	}
}

Resolving Provider from ProviderReference

The server resolves the v1 provider based on GVK:

func (s *AdapterServer) resolveProvider(ref *pb.ProviderReference) (esv1.ProviderInterface, error) {
	if ref == nil {
		return nil, fmt.Errorf("provider reference is nil")
	}

	splitted := strings.Split(ref.ApiVersion, "/")
	if len(splitted) != 2 {
		return nil, fmt.Errorf("invalid api version: %s", ref.ApiVersion)
	}
	group := splitted[0]
	version := splitted[1]

	key := schema.GroupVersionKind{
		Group:   group,
		Version: version,
		Kind:    ref.Kind,
	}
	v1Provider, ok := s.resourceMapping[key]
	if !ok {
		return nil, fmt.Errorf("resource mapping not found for %q", key)
	}
	return v1Provider, nil
}

func (s *AdapterServer) getClient(ctx context.Context, ref *pb.ProviderReference, namespace string) (esv1.SecretsClient, error) {
	if ref == nil {
		return nil, fmt.Errorf("request or remote ref is nil")
	}

	spec, err := s.specMapper(ref)
	if err != nil {
		return nil, fmt.Errorf("failed to map provider reference to spec: %w", err)
	}
	// TODO: support cluster scoped Provider
	store, err := NewSyntheticStore(spec, namespace)
	if err != nil {
		return nil, fmt.Errorf("failed to create synthetic store: %w", err)
	}
	provider, err := s.resolveProvider(ref)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve provider: %w", err)
	}
	return provider.NewClient(ctx, store, s.kubeClient, namespace)
}

GetSecret RPC Handler

The server receives GetSecret requests and delegates to v1 providers:

// GetSecret retrieves a single secret from the provider.
func (s *AdapterServer) GetSecret(ctx context.Context, req *pb.GetSecretRequest) (*pb.GetSecretResponse, error) {
	if req == nil || req.RemoteRef == nil {
		return nil, fmt.Errorf("request or remote ref is nil")
	}
	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
	if err != nil {
		return nil, fmt.Errorf("failed to get client: %w", err)
	}
	defer client.Close(ctx)

	// Convert protobuf remote ref to v1 remote ref
	ref := esv1.ExternalSecretDataRemoteRef{
		Key:      req.RemoteRef.Key,
		Version:  req.RemoteRef.Version,
		Property: req.RemoteRef.Property,
	}
	if req.RemoteRef.DecodingStrategy != "" {
		ref.DecodingStrategy = esv1.ExternalSecretDecodingStrategy(req.RemoteRef.DecodingStrategy)
	}
	if req.RemoteRef.MetadataPolicy != "" {
		ref.MetadataPolicy = esv1.ExternalSecretMetadataPolicy(req.RemoteRef.MetadataPolicy)
	}

	value, err := client.GetSecret(ctx, ref)
	if err != nil {
		return nil, fmt.Errorf("failed to get secret: %w", err)
	}

	return &pb.GetSecretResponse{
		Value: value,
	}, nil
}

AWS Provider Main: Single Process, Multiple APIs

The AWS provider's main function sets up the mapping:

v1Provider := awsv1.NewProvider()
adapterServer := adapter.NewAdapterServer(kubeClient, adapter.ProviderMapping{
	schema.GroupVersionKind{
		Group:   awsv2alpha1.GroupVersion.Group,
		Version: awsv2alpha1.GroupVersion.Version,
		Kind:    awsv2alpha1.SecretsManagerKind,
	}: v1Provider,
}, func(ref *pb.ProviderReference) (*v1.SecretStoreSpec, error) {
	if ref.Kind != awsv2alpha1.SecretsManagerKind {
		return nil, fmt.Errorf("unsupported provider kind: %s", ref.Kind)
	}
	var awsProvider awsv2alpha1.SecretsManager
	err := kubeClient.Get(context.Background(), client.ObjectKey{
		Namespace: ref.Namespace,
		Name:      ref.Name,
	}, &awsProvider)
	if err != nil {
		return nil, err
	}
	return &v1.SecretStoreSpec{
		Provider: &v1.SecretStoreProvider{
			AWS: &v1.AWSProvider{
				Service:           v1.AWSServiceSecretsManager,
				Auth:              awsProvider.Spec.Auth,
				Role:              awsProvider.Spec.Role,
				Region:            awsProvider.Spec.Region,
				AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
				ExternalID:        awsProvider.Spec.ExternalID,
				SecretsManager:    awsProvider.Spec.SecretsManager,
				SessionTags:       awsProvider.Spec.SessionTags,
				TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
				Prefix:            awsProvider.Spec.Prefix,
			},
		},
	}, nil
})

To add ParameterStore, you'd simply extend the mapping:

schema.GroupVersionKind{
    Group:   awsv2alpha1.GroupVersion.Group,
    Version: awsv2alpha1.GroupVersion.Version,
    Kind:    "ParameterStore",
}: v1Provider,  // Same v1 provider instance!

And update the specMapper to handle the new Kind.


Example manifest

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: external-secrets-system
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: Provider  # <-- 
    name: aws-provider
  target:
    name: creds  
  data:
  - secretKey: creds
    remoteRef:
      key: app-credentials
---
apiVersion: external-secrets.io/v1
kind: Provider
metadata:
  name: aws-provider
  namespace: external-secrets-system
spec:
  config:
    address: provider-aws.external-secrets-system.svc:8080
    providerRef:
      apiVersion: provider.external-secrets.io/v2alpha1
      kind: SecretsManager
      name: aws-sm
      namespace: external-secrets-system
---
apiVersion: provider.external-secrets.io/v2alpha1
kind: SecretsManager
metadata:
  name: aws-sm
  namespace: external-secrets-system
spec:
  region: eu-central-1
  auth: {}

@github-actions github-actions bot added size/xl kind/documentation Categorizes issue or PR as related to documentation. kind/dependency dependabot and upgrades component/github-actions and removed size/xl labels Oct 30, 2025
@moolen moolen marked this pull request as draft October 30, 2025 14:19
@moolen moolen force-pushed the mj-v2-poc branch 2 times, most recently from c150db0 to 233df5b Compare November 6, 2025 23:53
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Nov 6, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
3 Security Hotspots
7.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 5, 2026

This pr is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale This issue/Pull Request is stale and will be automatically closed label Feb 5, 2026
@Skarlso Skarlso removed the Stale This issue/Pull Request is stale and will be automatically closed label Feb 5, 2026
@Skarlso Skarlso moved this to In Progress in External Secrets Feb 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc2108a8-836f-4684-b62e-2bb32dada31b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

moolen added 9 commits March 9, 2026 21:18
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
moolen added 3 commits March 9, 2026 21:58
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
)

const (
errSecretStoreNotReady = "%s %q is not ready"

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

Hard-coded
secret
.
}

if meta.Spec.SecretType == "" {
meta.Spec.SecretType = "String"

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

Hard-coded
secret
.
// Declares metadata information for pushing secrets to AWS Secret Store.
const (
SecretPushFormatKey = "secretPushFormat"
SecretPushFormatString = "string"

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

Hard-coded
secret
.
const (
SecretPushFormatKey = "secretPushFormat"
SecretPushFormatString = "string"
SecretPushFormatBinary = "binary"

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

Hard-coded
secret
.
}

if meta.Spec.SecretPushFormat == "" {
meta.Spec.SecretPushFormat = SecretPushFormatBinary

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

Hard-coded
secret
.
// Format with goimports/gofmt
formattedMain, err := formatGoCode(mainContent)
if err != nil {
log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err)

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 23 days ago

In general, to avoid log forgery when logging user-controlled data, sanitize or encode that data before logging: for plain-text logs, strip newline (\n) and carriage return (\r) characters (and optionally other control characters) so an attacker cannot break or inject additional log lines. Then log the sanitized value instead of the raw user input.

For this specific code, the only problematic sink in the shown snippet is the usage of config.Provider.Name in log messages, notably on line 197: log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err). We should introduce a small helper that takes a string and returns a sanitized version with \n and \r removed (using strings.ReplaceAll per the recommendation), and then use that helper when interpolating config.Provider.Name into log messages. This preserves the semantics—provider names will still appear as-is for normal values—while mitigating the injection vector.

Concretely:

  • Add a function, e.g. sanitizeLogString(s string) string, near the bottom of providers/v2/hack/generate-provider-main.go (or anywhere appropriate in that file) that removes \n and \r using strings.ReplaceAll.
  • Update the log call around line 197 to pass sanitizeLogString(config.Provider.Name) instead of config.Provider.Name.
  • Optionally, for consistency and additional safety, apply the same helper to the other log calls that include config.Provider.Name in this file (lines 191 and 214), since they share the same tainted source. This doesn’t require new imports because strings is already imported.

No new external dependencies are needed; the standard library strings package is already in use.

Suggested changeset 1
providers/v2/hack/generate-provider-main.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/providers/v2/hack/generate-provider-main.go b/providers/v2/hack/generate-provider-main.go
--- a/providers/v2/hack/generate-provider-main.go
+++ b/providers/v2/hack/generate-provider-main.go
@@ -188,13 +188,13 @@
 		// Generate main.go
 		mainContent, err := executeTemplate(mainTemplate, templateData)
 		if err != nil {
-			log.Fatalf("Failed to generate main.go for %s: %v", config.Provider.Name, err)
+			log.Fatalf("Failed to generate main.go for %s: %v", sanitizeLogString(config.Provider.Name), err)
 		}
 
 		// Format with goimports/gofmt
 		formattedMain, err := formatGoCode(mainContent)
 		if err != nil {
-			log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err)
+			log.Printf("Warning: Failed to format main.go for %s: %v", sanitizeLogString(config.Provider.Name), err)
 			formattedMain = mainContent // Use unformatted if formatting fails
 		}
 
@@ -211,7 +206,7 @@
 		// Generate Dockerfile
 		dockerContent, err := executeTemplate(dockerfileTemplate, templateData)
 		if err != nil {
-			log.Fatalf("Failed to generate Dockerfile for %s: %v", config.Provider.Name, err)
+			log.Fatalf("Failed to generate Dockerfile for %s: %v", sanitizeLogString(config.Provider.Name), err)
 		}
 
 		dockerPath := filepath.Join(providerDir, "Dockerfile")
@@ -284,6 +279,14 @@
 	return &config, nil
 }
 
+// sanitizeLogString removes line breaks from strings before logging to prevent
+// log forgery or confusion caused by user-controlled values.
+func sanitizeLogString(s string) string {
+	s = strings.ReplaceAll(s, "\n", "")
+	s = strings.ReplaceAll(s, "\r", "")
+	return s
+}
+
 func loadTemplate(name string) (*template.Template, error) {
 	content, err := embeddedFS.ReadFile(name)
 	if err != nil {
EOF
@@ -188,13 +188,13 @@
// Generate main.go
mainContent, err := executeTemplate(mainTemplate, templateData)
if err != nil {
log.Fatalf("Failed to generate main.go for %s: %v", config.Provider.Name, err)
log.Fatalf("Failed to generate main.go for %s: %v", sanitizeLogString(config.Provider.Name), err)
}

// Format with goimports/gofmt
formattedMain, err := formatGoCode(mainContent)
if err != nil {
log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err)
log.Printf("Warning: Failed to format main.go for %s: %v", sanitizeLogString(config.Provider.Name), err)
formattedMain = mainContent // Use unformatted if formatting fails
}

@@ -211,7 +206,7 @@
// Generate Dockerfile
dockerContent, err := executeTemplate(dockerfileTemplate, templateData)
if err != nil {
log.Fatalf("Failed to generate Dockerfile for %s: %v", config.Provider.Name, err)
log.Fatalf("Failed to generate Dockerfile for %s: %v", sanitizeLogString(config.Provider.Name), err)
}

dockerPath := filepath.Join(providerDir, "Dockerfile")
@@ -284,6 +279,14 @@
return &config, nil
}

// sanitizeLogString removes line breaks from strings before logging to prevent
// log forgery or confusion caused by user-controlled values.
func sanitizeLogString(s string) string {
s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "")
return s
}

func loadTemplate(name string) (*template.Template, error) {
content, err := embeddedFS.ReadFile(name)
if err != nil {
Copilot is powered by AI and may make mistakes. Always verify output.
// Format with goimports/gofmt
formattedMain, err := formatGoCode(mainContent)
if err != nil {
log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err)

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
moolen added 2 commits March 10, 2026 00:29
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Mar 9, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots
18.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@moolen moolen changed the title draft: Provider v2 feat: Provider v2 Mar 10, 2026
@github-actions github-actions bot added the kind/feature Categorizes issue or PR as related to a new feature. label Mar 10, 2026
@Skarlso Skarlso self-requested a review March 11, 2026 19:26
@Skarlso Skarlso self-assigned this Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/github-actions kind/dependency dependabot and upgrades kind/documentation Categorizes issue or PR as related to documentation. kind/feature Categorizes issue or PR as related to a new feature. size/xl size/xs

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants