Upjet v2 introduces support for generating Crossplane v2 compatible providers with namespaced MRs.
This guide describes how to transition an existing Crossplane v1 Upjet-based provider to support Crossplane v2, using Upjet v2.
To allow smoother transitions for the provider consumers, Upjet v2 generates providers with both legacy cluster-scoped MRs and modern namespaced MRs.
Namespaced MRs are functionally equivalent to their cluster-scoped counterparts. To facilitate Crossplane v2 concepts, Upjet v2 has the following changes for namespaced MR APIs:
-
Namespace-scoped MRs should have the root API group as
acme.m.example.orgto differentiate from the cluster-scoped MRsacme.example.org. Although this is not enforced, it is strongly recommended to follow this convention. -
spec.providerConfigRefis now a typed reference, withkindandname. MRs can either reference a cluster-scoped or namespace-scoped provider config. Providers should introduce namespace-scopedProviderConfig.acme.m.example.organd cluster-scopedClusterProviderConfig.acme.m.example.org.- when omitted, defaults to
kind: "ClusterProviderConfig" name: "default".
- when omitted, defaults to
-
Secret references for sensitive input parameters (e.g.
spec.forProvider.fooSecretRef) and connection secrets (spec.writeConnectionSecretToRef) are now generated as local secret references. -
Cross-resource references are generated with optional namespace parameter, that defaults to the same namespace as the MRs. You can make cross-namespace cross-resource references.
-
Alpha feature External Secret Store support is dropped from Crossplane v2. Therefore
spec.publishConnectionDetailsTois removed from ALL MRs.
apiGroup: demogroup.acme.m.example.org
kind: DemoResource
metadata:
name: foo-demo
namespace: default
spec:
providerConfigRef:
kind: ClusterProviderConfig
name: some-pc
writeConnectionSecretToRef:
name: foo-demo-conn
forProvider:
coolField: "I am cool"
passwordSecretRef: # sensitive input parameter
name: very-important-secret # local k8s secret reference only
barRef: # cross-resource reference
name: some-bar-resource
namespace: other-ns # optional, defaults to same namespace as the MR.- Crossplane c2 has introduced support for
SafeStartcapability, which starts MR controllers after their CRDs become available. This needs to be implemented by the provider, as described in this guide.
Providers generated with Upjet v2 is backward compatible with Crossplane v1 environments, with following the notes:
- Providers still serve legacy cluster-scoped MRs as is.
- After upgrade, existing cluster-scoped MRs continue to work.
- Only exception is the removal of
spec.publishConnectionDetailsTowhich was an alpha feature, you need to remove those before upgrading if any usage.
SafeStartcapability will be disabled. The guide explains the implementation details for properly implementing the safe start.- Namespaced MRs still get installed alongside cluster-scoped MRs. They can be used standalone, but you cannot compose them in Crossplane v1.
You can refer to Crossplane v2 compatibility PR in the crossplane/upjet-provider-template repo.
- update to latest Upjet version that supports namespaced resources
- duplicate content into both cluster and namespaced copies and make some minor updates for api groups and import paths
- config, apis, controllers
- remove all legacy conversion/migration logic, because only the latest version needs to be supported
- update the provider main.go template to setup both cluster and namespaced apis and controllers
- update code generation comment markers to run on both cluster and namespaced types
- update the generator cmd to init both cluster and namespaced config to pass to code gen pipeline
- manually copy and update the handful of manual API and controller files to namespaced dirs
github.com/crossplane/crossplane-runtime/v2 v2.0.0
github.com/crossplane/crossplane-tools master
github.com/crossplane/upjet/v2 v2.1.0go mod tidycd build
git checkout main
git pull
# back to the repo root
cd ..In all of your source files, using your favorite editor or CLI tools like sed,
do the following replacements in your import paths
github.com/crossplane/crossplane-runtime/ => github.com/crossplane/crossplane-runtime/v2/
github.com/crossplane/upjet/ => github.com/crossplane/upjet/v2/
- in
apis/v1alpha1removeStoreConfigapi types and registration Example commit
- move
apis/toapis/cluster, exceptgenerate.go - create empty
apis/namespaceddirectory - copy only root api group folders to
apis/namespaced, e.g.v1alpha1,v1beta1and any manually authored files if any.
In the apis/namespaced/<version>/:
- update api group markers from
yourprovider.crossplane.iotoyourprovider.m.crossplane.io, typically inapis/namespaced/<version>/:
// Package type metadata.
const (
Group = "yourprovider.m.upbound.io"
Version = "v1beta1"
)- update
scopemarkers to namespaced
// +kubebuilder:resource:scope=Namespaced
type ProviderConfig struct {
...
}- In
apis/namespaced/v1beta1/types.go, make sure thatProviderConfigUsagetype inlinesxpv2.TypedProviderConfigUsage
import(
...
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+ xpv2 "github.com/crossplane/crossplane-runtime/apis/common/v2"
)
type ProviderConfigUsage struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
- xpv1.ProviderConfigUsage `json:",inline"`
+ xpv2.TypedProviderConfigUsage `json:",inline"`
}- Add the new
ClusterProviderConfigandClusterProviderConfigListtypes intoapis/namespaced/v1beta1/types.go- You can duplicate existing
ProviderConfigandProviderConfigListstruct definitions, and rename them - Ensure that it has the scope kubebuilder marker
Cluster - It is registered to the scheme
- See Example Commit
- You can duplicate existing
// +kubebuilder:object:root=true
// A ClusterProviderConfig configures the Template provider.
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:storageversion
type ClusterProviderConfig struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ProviderConfigSpec `json:"spec"`
Status ProviderConfigStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// ClusterProviderConfigList contains a list of ProviderConfig.
type ClusterProviderConfigList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ClusterProviderConfig `json:"items"`
}Update several dependencies to their latest versions. At the time of writing, these are the latest versions.
KIND_VERSION = v0.30.0
UP_VERSION = v0.41.0
UP_CHANNEL = stable
CROSSPLANE_VERSION = 2.0.2-
Move
internal/controllerto/internal/controller/cluster -
In
internal/controller/cluster/providerconfig/config.go, ensure you pass theUsagekind example -
In
internal/controller/namespaced/providerconfig/config.goadjust the setup function, so that it registers controllers for bothProviderConfigandClusterProviderConfigtypes. -
provider-config example commit
Ensure the provider config controller setup functions have a new variant SetupGated. This should register a func that wraps the original Setup call, and specify the GVKs to wait for before doing the controller setup.
internal/controller/cluster/providerconfig/config.go
// SetupGated adds a controller that reconciles ProviderConfigs by accounting for
// their current usage.
func SetupGated(mgr ctrl.Manager, o controller.Options) error {
o.Options.Gate.Register(func() {
if err := Setup(mgr, o); err != nil {
mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1beta1.ProviderConfigGroupVersionKind.String())
}
}, v1beta1.ProviderConfigGroupVersionKind, v1beta1.ProviderConfigUsageGroupVersionKind)
return nil
}internal/controller/namespaced/providerconfig/config.go
Note that, we specify 3 GVKs here.
// SetupGated adds a controller that reconciles ProviderConfigs by accounting for
// their current usage.
func SetupGated(mgr ctrl.Manager, o controller.Options) error {
o.Options.Gate.Register(func() {
if err := Setup(mgr, o); err != nil {
mgr.GetLogger().Error(err, "unable to setup reconcilers", "gvk", v1beta1.ClusterProviderConfigGroupVersionKind.String(), "gvk", v1beta1.ProviderConfigGroupVersionKind.String())
}
}, v1beta1.ClusterProviderConfigGroupVersionKind, v1beta1.ProviderConfigGroupVersionKind, v1beta1.ProviderConfigUsageGroupVersionKind)
return nil
}At this point, you should have 3 provider config API type.
- cluster-scoped
ProviderConfig.template.example.orgin the existing legacy API group - namespace-scoped
ProviderConfig.template.m.example.orgin the new modern API group - cluster-scoped
ClusterProviderConfig.template.m.example.orgin the new modern API group
Legacy cluster-scoped MRs should only resolve Legacy ProviderConfig references in the legacy API group.
Modern namespaced MRs should only resolve Modern provider config references, in the modern api group.
After resolving the referenced provider config, convert the resolved config to a common runtime type. ProviderConfigSpec type is recommended. This allows the rest of the logic to operate on a single type.
If there are namespaced references to the secrets, overwrite them with MR namespace.
Tip
See the example reference implementation in the provider template repo.
- duplicate individual resource configurators by their scope. It should be similar to:
config/
cluster/
foo/
config.go
bar/
config.go
namespaced/
foo/
config.go
bar/
config.go
provider-metadata.yaml
schema.json
provider.go
-
in
provider.go, duplicate theGetProvider()function and define theGetProviderNamespaced() -
Ensure root group name includes
.mto distinguish from cluster-scoped API group. -
Ensure the namespaced custom configurations are used for this provider
import (
//
fooCluster "github.com/upbound/upjet-provider-template/config/cluster/foo"
fooNamespaced "github.com/upbound/upjet-provider-template/config/namespaced/foo"
)
// ...
func GetProviderNamespaced() *ujconfig.Provider {
pc := ujconfig.NewProvider([]byte(providerSchema), resourcePrefix, modulePath, []byte(providerMetadata),
ujconfig.WithRootGroup("template.m.upbound.io"),
// ...
)
for _, configure := range []func(provider *ujconfig.Provider){
// add custom config functions
fooNamespaced.Configure,
} {
configure(pc)
}
pc.ConfigureResources()
return pc
}Tip
Check the example change upjet provider template.
pipeline run should be invoked with both cluster-scoped and namespace-scoped provider
- pipeline.Run(config.GetProvider(), absRootDir)
+ pipeline.Run(config.GetProvider(), config.GetProviderNamespaced(), absRootDir)- import both cluster-scoped and namespaced
apisandconfigpackages
+ authv1 "k8s.io/api/authorization/v1"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
- "github.com/upbound/upjet-provider-template/apis"
- "github.com/upbound/upjet-provider-template/apis/v1alpha1"
+ apisCluster "github.com/upbound/upjet-provider-template/apis/cluster"
+ apisNamespaced "github.com/upbound/upjet-provider-template/apis/namespaced"
"github.com/upbound/upjet-provider-template/config"
"github.com/upbound/upjet-provider-template/internal/clients"
- "github.com/upbound/upjet-provider-template/internal/controller"
+ controllerCluster "github.com/upbound/upjet-provider-template/internal/controller/cluster"
+ controllerNamespaced "github.com/upbound/upjet-provider-template/internal/controller/namespaced"
"github.com/upbound/upjet-provider-template/internal/features"
+ "github.com/upbound/upjet-provider-template/internal/version"- add both cluster-scoped and namespaced apis to
scheme - also add k8s authv1 APIs to the
scheme - remove external secret store options if any
- duplicate controller options,
clusterOptionsandnamespacedOptions
clusterOpts := tjcontroller.Options{
//...
Provider: config.GetProvider(),
}
namespacedOpts := tjcontroller.Options{
//...
Provider: config.GetProvider(),
}- make sure if you have some additional configuration, they are added to both, for example:
if *enableManagementPolicies {
clusterOpts.Features.Enable(features.EnableBetaManagementPolicies)
namespacedOpts.Features.Enable(features.EnableBetaManagementPolicies)
log.Info("Beta feature enabled", "flag", features.EnableBetaManagementPolicies)
}- setup both cluster-scoped and namespaced controllers
- implement safe-start capability
canSafeStart, err := canWatchCRD(context.TODO(), mgr)
kingpin.FatalIfError(err, "SafeStart precheck failed")
if canSafeStart {
crdGate := new(gate.Gate[schema.GroupVersionKind])
clusterOpts.Gate = crdGate
namespacedOpts.Gate = crdGate
kingpin.FatalIfError(customresourcesgate.Setup(mgr, xpcontroller.Options{
Logger: log,
Gate: crdGate,
MaxConcurrentReconciles: 1,
}), "Cannot setup CRD gate")
kingpin.FatalIfError(controllerCluster.SetupGated(mgr, clusterOpts), "Cannot setup cluster-scoped Template controllers")
kingpin.FatalIfError(controllerNamespaced.SetupGated(mgr, namespacedOpts), "Cannot setup namespaced Template controllers")
} else {
log.Info("Provider has missing RBAC permissions for watching CRDs, controller SafeStart capability will be disabled")
kingpin.FatalIfError(controllerCluster.Setup(mgr, clusterOpts), "Cannot setup cluster-scoped Template controllers")
kingpin.FatalIfError(controllerNamespaced.Setup(mgr, namespacedOpts), "Cannot setup namespaced Template controllers")
}
func canWatchCRD(ctx context.Context, mgr manager.Manager) (bool, error) {
if err := authv1.AddToScheme(mgr.GetScheme()); err != nil {
return false, err
}
verbs := []string{"get", "list", "watch"}
for _, verb := range verbs {
sar := &authv1.SelfSubjectAccessReview{
Spec: authv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authv1.ResourceAttributes{
Group: "apiextensions.k8s.io",
Resource: "customresourcedefinitions",
Verb: verb,
},
},
}
if err := mgr.GetClient().Create(ctx, sar); err != nil {
return false, errors.Wrapf(err, "unable to perform RBAC check for verb %s on CustomResourceDefinitions", verbs)
}
if !sar.Status.Allowed {
return false, nil
}
}
return true, nil
}Tip
Check the example commit in the upjet provider template.
After implementing the safe start capability in the above steps,
mark your provider as SafeStart capable in the package metadata.
apiVersion: meta.pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-azuread
annotations:
...
spec:
+ capabilities:
+ - SafeStart- fully remove the contents of this directory, it will be regenerated
Add examples for the new API groups.
- Update
apiGroupto foo.m.crossplane.io - add
metadata.namespace - remove
namespacefrom allSecretReffields inspec.forProviderif any - remove
namespacefromspec.writeConnectionSecretToRef
If you have an Uptest setup script and you create a default provider config
for tests, make sure that you create a provider config with the new
API group ClusterProviderConfig.foo.m.crossplane.io.
make generateAfter generating the provider:
- check
apis/namespaceddirectory and ensure namespaced types are generated. - check
internal/controller/namespaceddirectory for namespaced controllers - check the
package/crdsdirectory and ensure namespaced CRDs are generated. - do a local deploy of the provider
make local-deployThis will create a kind cluster with your provider deployed. Apply some example MRs to validate. Optionally use Uptest.