Skip to content

Commit c1f6267

Browse files
committed
clusterctl: implement CRD name precheck
Signed-off-by: Stefan Büringer [email protected]
1 parent 5b4efa1 commit c1f6267

File tree

6 files changed

+138
-2
lines changed

6 files changed

+138
-2
lines changed

cmd/clusterctl/api/v1alpha3/annotations.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ package v1alpha3
1919
const (
2020
// CertManagerVersionAnnotation reports the cert manager version installed by clusterctl.
2121
CertManagerVersionAnnotation = "cert-manager.clusterctl.cluster.x-k8s.io/version"
22+
23+
// SkipCRDNamePreflightCheckAnnotation can be placed on provider CRDs, so that clusterctl doesn't emit a
24+
// warning if the CRD doesn't comply with Cluster APIs naming scheme.
25+
// Note: Only CRDs that are referenced by core Cluster API CRDs have to comply with the naming scheme.
26+
// See the following issue for more information: https://github.com/kubernetes-sigs/cluster-api/issues/5686#issuecomment-1260897278
27+
SkipCRDNamePreflightCheckAnnotation = "clusterctl.cluster.x-k8s.io/skip-crd-name-preflight-check"
2228
)

cmd/clusterctl/client/cluster/installer.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import (
2525
"github.com/pkg/errors"
2626
appsv1 "k8s.io/api/apps/v1"
2727
corev1 "k8s.io/api/core/v1"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
2830
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31+
"k8s.io/apimachinery/pkg/runtime/schema"
2932
"k8s.io/apimachinery/pkg/util/sets"
3033
"k8s.io/apimachinery/pkg/util/version"
3134
"k8s.io/apimachinery/pkg/util/wait"
@@ -35,8 +38,10 @@ import (
3538
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
3639
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
3740
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
41+
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
3842
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
3943
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
44+
"sigs.k8s.io/cluster-api/util/contract"
4045
)
4146

4247
// ProviderInstaller defines methods for enforcing consistency rules for provider installation.
@@ -53,6 +58,8 @@ type ProviderInstaller interface {
5358
// The following checks are performed in order to ensure a fully operational cluster:
5459
// - There must be only one instance of the same provider
5560
// - All the providers in must support the same API Version of Cluster API (contract)
61+
// - All provider CRDs that are referenced in core Cluster API CRDs must comply with the CRD naming scheme,
62+
// otherwise a warning is logged.
5663
Validate() error
5764

5865
// Images returns the list of images required for installing the providers ready in the install queue.
@@ -209,9 +216,75 @@ func (i *providerInstaller) Validate() error {
209216
return errors.Errorf("installing provider %q can lead to a non functioning management cluster: the target version for the provider supports the %s API Version of Cluster API (contract), while the management cluster is using %s", components.ManifestLabel(), providerContract, managementClusterContract)
210217
}
211218
}
219+
220+
// Validate if provider CRDs comply with the naming scheme.
221+
for _, components := range i.installQueue {
222+
componentObjects := components.Objs()
223+
224+
for _, obj := range componentObjects {
225+
// Continue if object is not a CRD.
226+
if obj.GetKind() != customResourceDefinitionKind {
227+
continue
228+
}
229+
230+
gk, err := getCRDGroupKind(obj)
231+
if err != nil {
232+
return errors.Wrap(err, "Failed to read group and kind from CustomResourceDefinition")
233+
}
234+
235+
if err := validateCRDName(obj, gk); err != nil {
236+
logf.Log.Info(err.Error())
237+
}
238+
}
239+
}
240+
212241
return nil
213242
}
214243

244+
func getCRDGroupKind(obj unstructured.Unstructured) (*schema.GroupKind, error) {
245+
var group string
246+
var kind string
247+
version := obj.GroupVersionKind().Version
248+
switch version {
249+
case apiextensionsv1beta1.SchemeGroupVersion.Version:
250+
crd := &apiextensionsv1beta1.CustomResourceDefinition{}
251+
if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil {
252+
return nil, errors.Wrapf(err, "failed to convert %s CustomResourceDefinition %q", version, obj.GetName())
253+
}
254+
group = crd.Spec.Group
255+
kind = crd.Spec.Names.Kind
256+
case apiextensionsv1.SchemeGroupVersion.Version:
257+
crd := &apiextensionsv1.CustomResourceDefinition{}
258+
if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil {
259+
return nil, errors.Wrapf(err, "failed to convert %s CustomResourceDefinition %q", version, obj.GetName())
260+
}
261+
group = crd.Spec.Group
262+
kind = crd.Spec.Names.Kind
263+
default:
264+
return nil, errors.Errorf("failed to read %s CustomResourceDefinition %q", version, obj.GetName())
265+
}
266+
return &schema.GroupKind{Group: group, Kind: kind}, nil
267+
}
268+
269+
func validateCRDName(obj unstructured.Unstructured, gk *schema.GroupKind) error {
270+
// Return if CRD has skip CRD name preflight check annotation.
271+
if _, ok := obj.GetAnnotations()[clusterctlv1.SkipCRDNamePreflightCheckAnnotation]; ok {
272+
return nil
273+
}
274+
275+
correctCRDName := contract.CalculateCRDName(gk.Group, gk.Kind)
276+
277+
// Return if name is correct.
278+
if obj.GetName() == correctCRDName {
279+
return nil
280+
}
281+
282+
return errors.Errorf("WARNING: CRD name %q is invalid for a CRD referenced in a core Cluster API CRD,"+
283+
"it should be %q. Support for CRDs that are referenced in core Cluster API resources with invalid names will be "+
284+
"dropped in a future Cluster API release. Note: Please check if this CRD is actually referenced in core Cluster API "+
285+
"CRDs. If not, this warning can be hidden by setting the %q' annotation.", obj.GetName(), correctCRDName, clusterctlv1.SkipCRDNamePreflightCheckAnnotation)
286+
}
287+
215288
// getProviderContract returns the API Version of Cluster API (contract) for a provider instance.
216289
func (i *providerInstaller) getProviderContract(providerInstanceContracts map[string]string, provider clusterctlv1.Provider) (string, error) {
217290
// If the contract for the provider instance is already known, return it.

cmd/clusterctl/client/cluster/installer_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
. "github.com/onsi/gomega"
2323
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
2425

2526
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
2627
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
@@ -257,6 +258,49 @@ func Test_providerInstaller_Validate(t *testing.T) {
257258
}
258259
}
259260

261+
func Test_providerInstaller_ValidateCRDName(t *testing.T) {
262+
tests := []struct {
263+
name string
264+
crd unstructured.Unstructured
265+
gk *schema.GroupKind
266+
wantErr bool
267+
}{
268+
{
269+
name: "CRD with valid name",
270+
crd: newFakeCRD("clusterclasses.cluster.x-k8s.io", nil),
271+
gk: &schema.GroupKind{Group: "cluster.x-k8s.io", Kind: "ClusterClass"},
272+
wantErr: false,
273+
},
274+
{
275+
name: "CRD with invalid name",
276+
crd: newFakeCRD("clusterclass.cluster.x-k8s.io", nil),
277+
gk: &schema.GroupKind{Group: "cluster.x-k8s.io", Kind: "ClusterClass"},
278+
wantErr: true,
279+
},
280+
{
281+
name: "CRD with invalid name but has skip annotation",
282+
crd: newFakeCRD("clusterclass.cluster.x-k8s.io", map[string]string{
283+
clusterctlv1.SkipCRDNamePreflightCheckAnnotation: "",
284+
}),
285+
gk: &schema.GroupKind{Group: "cluster.x-k8s.io", Kind: "ClusterClass"},
286+
wantErr: false,
287+
},
288+
}
289+
290+
for _, tt := range tests {
291+
t.Run(tt.name, func(t *testing.T) {
292+
g := NewWithT(t)
293+
294+
err := validateCRDName(tt.crd, tt.gk)
295+
if tt.wantErr {
296+
g.Expect(err).To(HaveOccurred())
297+
} else {
298+
g.Expect(err).NotTo(HaveOccurred())
299+
}
300+
})
301+
}
302+
}
303+
260304
type fakeComponents struct {
261305
config.Provider
262306
inventoryObject clusterctlv1.Provider
@@ -283,7 +327,7 @@ func (c *fakeComponents) InventoryObject() clusterctlv1.Provider {
283327
}
284328

285329
func (c *fakeComponents) Objs() []unstructured.Unstructured {
286-
panic("not implemented")
330+
return []unstructured.Unstructured{}
287331
}
288332

289333
func (c *fakeComponents) Yaml() ([]byte, error) {
@@ -297,3 +341,10 @@ func newFakeComponents(name string, providerType clusterctlv1.ProviderType, vers
297341
inventoryObject: inventoryObject,
298342
}
299343
}
344+
345+
func newFakeCRD(name string, annotations map[string]string) unstructured.Unstructured {
346+
u := unstructured.Unstructured{}
347+
u.SetName(name)
348+
u.SetAnnotations(annotations)
349+
return u
350+
}

cmd/clusterctl/client/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ func (c *clusterctlClient) Init(options InitOptions) ([]Components, error) {
117117
// Before installing the providers, validates the management cluster resulting by the planned installation. The following checks are performed:
118118
// - There should be only one instance of the same provider.
119119
// - All the providers must support the same API Version of Cluster API (contract)
120+
// - All provider CRDs that are referenced in core Cluster API CRDs must comply with the CRD naming scheme,
121+
// otherwise a warning is logged.
120122
if err := installer.Validate(); err != nil {
121123
if !options.IgnoreValidationErrors {
122124
return nil, err

docs/book/src/developer/providers/v1.2-to-v1.3.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ The default value is 0, meaning that the volume can be detached without any time
4545

4646
### Other
4747

48+
- clusterctl now emits a warning for provider CRDs which don't comply with the CRD naming conventions. This warning can be skipped for resources not referenced by Cluster API
49+
core resources via the `clusterctl.cluster.x-k8s.io/skip-crd-name-preflight-check` annotation. The contracts specify:
50+
> The CRD name must have the format produced by sigs.k8s.io/cluster-api/util/contract.CalculateCRDName(Group, Kind)
4851
- The Kubernetes default registry has been changed from `k8s.gcr.io` to `registry.k8s.io`. Kubernetes image promotion currently publishes to both registries. Please
4952
consider publishing manifests which reference the controller images from the new registry (for reference [Cluster API PR](https://github.com/kubernetes-sigs/cluster-api/pull/7478)).
5053
- e2e tests are upgraded to use Ginkgo v2 (v2.4.0) and Gomega v1.22.1. Providers who use the test framework from this release will also need to upgrade, because Ginkgo v2 can't be imported alongside v1. Please see the [Ginkgo upgrade guide](https://onsi.github.io/ginkgo/MIGRATING_TO_V2), and note:

docs/book/src/reference/labels_and_annotations.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
| Annotation | Note |
2222
|:--------|:--------|
23+
| clusterctl.cluster.x-k8s.io/skip-crd-name-preflight-check | Can be placed on provider CRDs, so that clusterctl doesn't emit a warning if the CRD doesn't comply with Cluster APIs naming scheme. Only CRDs that are referenced by core Cluster API CRDs have to comply with the naming scheme. |
2324
| unsafe.topology.cluster.x-k8s.io/disable-update-class-name-check | It can be used to disable the webhook check on update that disallows a pre-existing Cluster to be populated with Topology information and Class. |
2425
| cluster.x-k8s.io/cluster-name | It is set on nodes identifying the name of the cluster the node belongs to. |
2526
|cluster.x-k8s.io/cluster-namespace | It is set on nodes identifying the namespace of the cluster the node belongs to. |
@@ -44,4 +45,4 @@
4445
| machinedeployment.clusters.x-k8s.io/max-replicas | It is the maximum replicas a deployment can have at a given point, which is machinedeployment.spec.replicas + maxSurge. Used by the underlying machine sets to estimate their proportions in case the deployment has surge replicas. |
4546
| controlplane.cluster.x-k8s.io/skip-coredns | It explicitly skips reconciling CoreDNS if set. |
4647
|controlplane.cluster.x-k8s.io/skip-kube-proxy | It explicitly skips reconciling kube-proxy if set.|
47-
| controlplane.cluster.x-k8s.io/kubeadm-cluster-configuration| It is a machine annotation that stores the json-marshalled string of KCP ClusterConfiguration. This annotation is used to detect any changes in ClusterConfiguration and trigger machine rollout in KCP.|
48+
| controlplane.cluster.x-k8s.io/kubeadm-cluster-configuration| It is a machine annotation that stores the json-marshalled string of KCP ClusterConfiguration. This annotation is used to detect any changes in ClusterConfiguration and trigger machine rollout in KCP.|

0 commit comments

Comments
 (0)