Purpose: For developers and platform engineers, explains the internal service plugin system in openCenter CLI, uses cert-manager as the worked example, and maps the practical code changes required to add a new platform service.
What This Plugin Mechanism Is
The internal service plugin system is how openCenter models platform services that are:
-
configured inside the cluster YAML
-
enabled or disabled with
opencenter cluster service … -
validated as part of cluster configuration
-
rendered into GitOps templates and manifests
Examples include:
-
cert-manager
-
Loki
-
Harbor
-
Keycloak
-
Velero
This is different from the external CLI plugin mechanism. External CLI plugins add new commands such as opencenter foo; they do not participate in service config typing, service validation, or GitOps manifest generation.
For that separate mechanism, see plugin-external-cli.md[Plugin External CLI].
The Service Plugin Model
At a high level, a service such as cert-manager is not just a Go plugin object. It is the combination of:
-
A typed configuration struct.
-
A config-type registration entry so the CLI can instantiate that struct by service name.
-
A service plugin implementation that provides metadata, validation, status, and an optional render hook.
-
Optional validation-engine extensions.
-
GitOps templates that actually produce the Kubernetes and Flux manifests.
-
CLI wiring for
cluster service enable,cluster service options, and service-specific secrets.
The core contract is the ServicePlugin interface with:
-
Name -
Type -
Validate -
Render -
Status
Evidence:
-
internal/services/plugin.go -
internal/services/base_plugin.go -
internal/services/registry.go
Base Plugin Composition
Most built-in services use BaseServicePlugin rather than implementing every method from scratch.
BaseServicePlugin provides:
-
standard metadata storage
-
a validator callback
-
a renderer callback
-
a status callback
-
default no-op behavior where needed
The common pattern is:
-
create a base plugin with metadata
-
embed it in a service-specific struct
-
inject service-specific validation/render/status functions
Evidence:
-
internal/services/base_plugin.go -
internal/services/plugins/cert_manager.go
Cert-Manager Walkthrough
Configuration Registration
cert-manager starts with a typed config struct:
-
CertManagerConfigembedsBaseConfig -
it adds fields such as
letsencrypt_server,email,region,dns_zones,issuers, anddns_provider -
its
init()function registers the config type under the namecert-manager
That registration is what lets generic CLI logic look up a service by name and instantiate the correct struct dynamically.
Evidence:
-
internal/config/services/cert_manager.go -
internal/config/registry/registry.go
Default Behavior
When a new cluster configuration is built, cert-manager is defaulted into the services map for at least the OpenStack provider path. The defaults include:
-
enabled: true -
a default support email
-
a default Route53 region
-
the production Let’s Encrypt ACME URL
Evidence:
-
internal/config/defaults.go
Enabling the Service from the CLI
When you run:
opencenter cluster service enable cert-manager --param="email=admin@example.com"
the CLI takes a mostly generic path:
-
Look up the service config type in the config registry.
-
Instantiate it with reflection.
-
Set
Enabled = true. -
Apply
--paramvalues by matching CLI keys to JSON tags on struct fields. -
Apply
--secretvalues by routing into the service’s secret struct. -
Run service-specific checks.
-
Save the updated cluster config.
-
Optionally render just that service with
--render.
For cert-manager specifically, the CLI also hard-requires email.
One important implementation detail: the CLI help for service-specific options and secrets is manually maintained. It is not generated from the service config struct. Adding a new service usually means updating getServiceOptions, getServiceSecrets, and service-specific validation logic yourself.
Evidence:
-
cmd/cluster_service.go
The Cert-Manager Plugin Object
The cert-manager service plugin itself is intentionally small.
NewCertManagerPlugin():
-
creates a base plugin with metadata
-
injects a cert-manager validator
-
injects a cert-manager status function
-
injects a render function that is currently just a placeholder
The current cert-manager validator checks only a few service-local rules:
-
letsencrypt_servermust usehttps:// -
emailmust contain@
The status function reports:
-
disabled state when the service is off
-
config status when it is enabled
-
details such as ACME server and email address
Evidence:
-
internal/services/plugins/cert_manager.go
Dependencies and Validation Metadata
Built-in services are also registered with dependency metadata.
For example:
-
cert-managerhas no dependencies -
keycloakdepends oncert-manager -
harbordepends oncert-manager
That dependency graph can be topologically sorted by the service registry. The registry also hosts service-specific validators such as service:cert-manager.
Evidence:
-
internal/services/plugins/registry.go -
internal/services/plugins/validators.go -
internal/services/registry.go
How Rendering Actually Happens Today
This is the most important nuance in the current implementation:
The production cluster generate path does not call ServicePlugin.Render() for cert-manager. Instead, it renders by walking the embedded GitOps template tree directly.
The current setup flow is:
-
Copy the base GitOps repository structure.
-
Render the cluster application overlays from
templates/cluster-apps-base. -
Render the infrastructure templates.
-
Run OpenTofu provisioning when needed.
So although the service plugin has a Render() method, the real cert-manager behavior currently comes mostly from templates, config types, and CLI wiring.
Evidence:
-
internal/cluster/setup_service.go -
internal/gitops/copy.go -
internal/gitops/embed.go
Cert-Manager Template Behavior
For cert-manager, the rendered output is assembled from several template families:
-
services/sources/opencenter-cert-manager.yaml.tpl -
services/fluxcd/cert-manager.yaml.tpl -
services/cert-manager/*.tpl
These templates generate the Flux source wiring, Flux Kustomizations, overlay issuer resources, overlay secrets, and Kustomize resources.
The cert-manager templates read values directly from the cluster config, including:
-
Email -
LetsEncryptServer -
Region -
cluster name
-
cluster FQDN
Evidence:
-
internal/gitops/templates/cluster-apps-base/services/sources/opencenter-cert-manager.yaml.tpl -
internal/gitops/templates/cluster-apps-base/services/fluxcd/cert-manager.yaml.tpl -
internal/gitops/templates/cluster-apps-base/services/cert-manager/letsencrypt-issuer.yaml.tpl -
internal/gitops/templates/cluster-apps-base/services/cert-manager/kustomization.yaml.tpl
Secrets Fallback Behavior
The AWS credentials used by the cert-manager Route53 secret follow a fallback chain:
-
service-specific cert-manager credentials
-
global application AWS credentials
-
global infrastructure AWS credentials
That logic lives on the main Config type so templates can call helper methods directly.
Evidence:
-
internal/config/config.go -
internal/gitops/templates/cluster-apps-base/services/cert-manager/opencenter-aws-credentials-secret.yaml.tpl
Inclusion Versus Presence
The renderer controls service behavior through two different mechanisms:
-
file-level skipping for disabled service directories
-
kustomization-level conditional inclusion for source and Flux wiring files
So a file may exist in the rendered overlay tree, but it only becomes active when the aggregate kustomization.yaml includes it.
Evidence:
-
internal/gitops/copy.go -
internal/gitops/templates/cluster-apps-base/services/sources/kustomization.yaml.tpl -
internal/gitops/templates/cluster-apps-base/services/fluxcd/kustomization.yaml.tpl
The Newer Registry-Based Path
The repository also contains a stage-based rendering model built around:
-
a global template registry that scans embedded templates
-
a service stage that resolves enabled services
-
template dependency resolution
-
stage-based rendering and validation
That model is conceptually cleaner and is useful to understand, but the main production setup path still uses the direct file-walk renderer described above.
Evidence:
-
internal/template/global_registry.go -
internal/template/embedded_registry.go -
internal/gitops/stages/service_stage.go
How To Add a New Internal Service Plugin
If you want to add a new platform service like cert-manager, the practical path today is to add a built-in service to this repository.
Step 1: Add the Typed Config
Create internal/config/services/<service>.go:
-
embed
BaseConfig -
add service-specific fields
-
register the config in
init()withregistry.RegisterServiceConfig("<service>", MyServiceConfig{})
This is required so cluster service enable <service> can instantiate the correct type.
Step 2: Add Defaults
Add the service to the default services map in internal/config/defaults.go.
Without this, new cluster configs will not consistently include or initialize the service.
Step 3: Add the Service Plugin
Create internal/services/plugins/<service>.go and model it after cert-manager, Loki, Harbor, or Velero:
type MyServiceConfig struct {
BaseConfig `yaml:",inline"`
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"`
}
func init() {
registry.RegisterServiceConfig("my-service", MyServiceConfig{})
}
type MyServicePlugin struct {
*svc.BaseServicePlugin
}
func NewMyServicePlugin() svc.ServicePlugin {
base := svc.NewBasePlugin(svc.PluginMetadata{
Name: "my-service",
Version: "1.0.0",
Description: "My custom platform service",
Type: svc.ServiceTypeCore,
Author: "opencenter",
License: "Apache-2.0",
})
p := &MyServicePlugin{BaseServicePlugin: base}
base.SetValidator(p.validate)
base.SetStatusFunc(p.status)
return p
}
Step 4: Register the Service and Dependencies
Update internal/services/plugins/registry.go:
-
add
NewMyServicePlugin()to the built-in service list -
declare dependencies such as
cert-managerif your service requires TLS or issuers
If you want validation-engine integration, also add a validator in internal/services/plugins/validators.go.
Step 5: Wire the CLI Management Path
Update cmd/cluster_service.go:
-
getServiceOptions -
getServiceSecrets -
validateService -
processSecretsservice-to-field mapping if the service has secrets
This is necessary because the current CLI service-management path is only partially generic.
Step 6: Add Secret Types and Fallback Helpers
If your templates need secrets:
-
add the service secret struct in the main config types
-
add helper methods on
Configif templates need fallback logic
Cert-manager is the best example because its templates call config helper methods directly.
Step 7: Add GitOps Templates
At minimum, add:
-
internal/gitops/templates/cluster-apps-base/services/<service>/… -
internal/gitops/templates/cluster-apps-base/services/sources/opencenter-<service>.yaml.tpl -
internal/gitops/templates/cluster-apps-base/services/fluxcd/<service>.yaml.tpl
Those exact file names matter. RenderSingleService() looks for:
-
a service directory named after the service
-
a source file named
opencenter-<service>.yamlor.yaml.tpl -
a Flux file named
<service>.yamlor.yaml.tpl
If the naming does not match, opencenter cluster service enable <service> --render will miss the files.
Step 8: Update Aggregate Kustomizations
If the new service should be selectable through the normal overlay structure, update the aggregate kustomization templates:
-
services/sources/kustomization.yaml.tpl -
services/fluxcd/kustomization.yaml.tpl
Those files decide whether the rendered source and Flux files are actually included.
Step 9: Update Template Registry Inference If Needed
If you want the stage-based template registry path to recognize the new service automatically, add the service name to the hard-coded list in internal/template/embedded_registry.go.
The direct render path does not need this, but the template-registry path does.
Related Reading
-
plugin-external-cli.md[Plugin External CLI]
-
services-templates.md[Service Templates]
-
../contributing/adding-services.md[Adding Services]
-
../contributing/code-structure.md[Code Structure]
-
gitops-workflow.md[GitOps Workflow]
Evidence
Primary code paths referenced in this explanation:
-
cmd/cluster_service.go -
internal/config/services/cert_manager.go -
internal/config/registry/registry.go -
internal/config/defaults.go -
internal/config/config.go -
internal/services/plugin.go -
internal/services/base_plugin.go -
internal/services/registry.go -
internal/services/plugins/cert_manager.go -
internal/services/plugins/registry.go -
internal/services/plugins/validators.go -
internal/cluster/setup_service.go -
internal/gitops/copy.go -
internal/gitops/embed.go -
internal/gitops/stages/service_stage.go -
internal/template/global_registry.go -
internal/template/embedded_registry.go