V2 providers run as standalone gRPC server processes, each requiring a main.go file with startup logic and a Dockerfile for containerization. This startup code follows a consistent pattern across all providers:
Provider-specific configuration includes:
SecretStoreSpecManually maintaining this boilerplate across multiple providers creates maintenance burden, increases error probability, and makes cross-cutting changes (like adding new flags or improving shutdown logic) require updates to every provider.
Without code generation, each provider requires:
main.gomain.go filesThe only true differences between providers are:
Implement a template-based code generator that produces main.go and Dockerfile from declarative YAML configuration.
Introduce a Generator Tool:
provider.yaml files in the providers directorygoimportsProvider Configuration (provider.yaml):
provider:
name: aws
displayName: "AWS Provider"
v2Package: "github.com/.../apis/provider/aws/v2alpha1"
stores:
- gvk:
group: "provider.external-secrets.io"
version: "v2alpha1"
kind: "SecretsManager"
v1Provider: "github.com/.../providers/v2/aws/store"
v1ProviderFunc: "NewProvider"
generators:
- gvk:
group: "generators.external-secrets.io"
version: "v1alpha1"
kind: "ECRAuthorizationToken"
v1Generator: "github.com/.../providers/v2/aws/generator"
v1GeneratorFunc: "NewECRGenerator"
configPackage: "."
Manual Component (config.go):
Provider-specific logic that cannot be templated remains in manually written config.go:
func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.SecretStoreSpec, error) {
return func(ref *pb.ProviderReference) (*v1.SecretStoreSpec, error) {
var provider awsv2alpha1.SecretsManager
err := kubeClient.Get(context.Background(), client.ObjectKey{
Namespace: ref.Namespace,
Name: ref.Name,
}, &provider)
if err != nil {
return nil, err
}
return &v1.SecretStoreSpec{
Provider: &v1.SecretStoreProvider{
AWS: &provider.Spec,
},
}, nil
}
}
providers/v2/ to find all provider.yaml filesmain.go.tmpl templateDockerfile.tmpl templategoimports to format and organize importsThe JSON schema enforces:
provider.name, provider.displayName)stores or generators must be definedMakefile targets provide interface to the generator:
make generate-providers # Generate all provider files
make verify-providers # Check if files are up-to-date
CI verification ensures generated files remain synchronized with configuration.
goimports for formattingmain.go—must modify template or configurationGetSpecMapper) remains manual, requiring judgment about what to templateRejected because maintenance burden scales linearly with provider count and cross-cutting changes become expensive.
Rejected because providers need different combinations of stores and generators, and compile-time type safety requires different imports and initialization code per provider.
Rejected because Go's static typing requires compile-time knowledge of which provider implementations to link, and dynamic loading has security and deployment implications.
The generator intentionally keeps the spec mapper logic manual because it involves provider-specific type conversions that vary significantly between providers. Templating this logic would create more complexity than it eliminates.
Future enhancements may include automatic discovery of v1 providers to reduce configuration requirements further.