diff --git a/cmd/clusterctl/client/repository/metadata_client.go b/cmd/clusterctl/client/repository/metadata_client.go index 84a7897b575a..9ea7adb44eb4 100644 --- a/cmd/clusterctl/client/repository/metadata_client.go +++ b/cmd/clusterctl/client/repository/metadata_client.go @@ -92,7 +92,36 @@ func (f *metadataClient) Get(ctx context.Context) (*clusterctlv1.Metadata, error return nil, errors.Wrapf(err, "error decoding %q for provider %q", metadataFile, f.provider.ManifestLabel()) } - //TODO: consider if to add metadata validation (TBD) + if err := validateMetadata(obj, f.provider.ManifestLabel()); err != nil { + return nil, err + } return obj, nil } + +// validateMetadata validates the metadata object structure. +// +// It checks if: +// 1. The metadata has the correct apiVersion and kind. +// 2. The metadata has at least one release series. +// +// Note: Version matching against releaseSeries is done later in `installer.go`. +func validateMetadata(metadata *clusterctlv1.Metadata, providerLabel string) error { + // Check if metadata has the correct apiVersion and kind + if metadata.APIVersion != clusterctlv1.GroupVersion.String() { + return errors.Errorf("invalid provider metadata: unexpected apiVersion %q for provider %s (expected %q)", + metadata.APIVersion, providerLabel, clusterctlv1.GroupVersion.String()) + } + + if metadata.Kind != "Metadata" { + return errors.Errorf("invalid provider metadata: unexpected kind %q for provider %s (expected \"Metadata\")", + metadata.Kind, providerLabel) + } + + // Check if metadata has at least one release series + if len(metadata.ReleaseSeries) == 0 { + return errors.Errorf("invalid provider metadata: releaseSeries is empty in metadata.yaml for provider %s", providerLabel) + } + + return nil +} diff --git a/cmd/clusterctl/client/repository/metadata_client_test.go b/cmd/clusterctl/client/repository/metadata_client_test.go index e194784e47f6..b3d20e28d0d1 100644 --- a/cmd/clusterctl/client/repository/metadata_client_test.go +++ b/cmd/clusterctl/client/repository/metadata_client_test.go @@ -135,3 +135,85 @@ func Test_metadataClient_Get(t *testing.T) { }) } } + +func Test_validateMetadata(t *testing.T) { + tests := []struct { + name string + metadata *clusterctlv1.Metadata + providerLabel string + wantErr bool + errMessage string + }{ + { + name: "valid metadata", + metadata: &clusterctlv1.Metadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterctlv1.GroupVersion.String(), + Kind: "Metadata", + }, + ReleaseSeries: []clusterctlv1.ReleaseSeries{ + {Major: 1, Minor: 0, Contract: "v1beta1"}, + }, + }, + providerLabel: "infra-test", + wantErr: false, + }, + { + name: "invalid apiVersion", + metadata: &clusterctlv1.Metadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "wrong.group/v1", + Kind: "Metadata", + }, + ReleaseSeries: []clusterctlv1.ReleaseSeries{ + {Major: 1, Minor: 0, Contract: "v1beta1"}, + }, + }, + providerLabel: "infra-test", + wantErr: true, + errMessage: "invalid provider metadata: unexpected apiVersion \"wrong.group/v1\" for provider infra-test (expected \"clusterctl.cluster.x-k8s.io/v1alpha3\")", + }, + { + name: "invalid kind", + metadata: &clusterctlv1.Metadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterctlv1.GroupVersion.String(), + Kind: "WrongKind", + }, + ReleaseSeries: []clusterctlv1.ReleaseSeries{ + {Major: 1, Minor: 0, Contract: "v1beta1"}, + }, + }, + providerLabel: "infra-test", + wantErr: true, + errMessage: "invalid provider metadata: unexpected kind \"WrongKind\" for provider infra-test (expected \"Metadata\")", + }, + { + name: "empty releaseSeries", + metadata: &clusterctlv1.Metadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterctlv1.GroupVersion.String(), + Kind: "Metadata", + }, + ReleaseSeries: []clusterctlv1.ReleaseSeries{}, + }, + providerLabel: "infra-test", + wantErr: true, + errMessage: "invalid provider metadata: releaseSeries is empty in metadata.yaml for provider infra-test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := validateMetadata(tt.metadata, tt.providerLabel) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errMessage)) + return + } + g.Expect(err).ToNot(HaveOccurred()) + }) + } +} diff --git a/docs/book/src/developer/providers/contracts/clusterctl.md b/docs/book/src/developer/providers/contracts/clusterctl.md index cfd3ee024fdb..340f3c3a334a 100644 --- a/docs/book/src/developer/providers/contracts/clusterctl.md +++ b/docs/book/src/developer/providers/contracts/clusterctl.md @@ -201,6 +201,21 @@ releaseSeries: contract: v1alpha2 ``` +#### Validation Rules + +Starting from clusterctl v1.11, the metadata YAML file is subject to strict validation to ensure consistency and prevent configuration errors. The following validation rules are enforced: + +1. **apiVersion**: Must be set to `clusterctl.cluster.x-k8s.io/v1alpha3` + * This ensures compatibility with the current clusterctl metadata format + +2. **kind**: Must be set to `Metadata` + * This identifies the resource type correctly + +3. **releaseSeries**: Must contain at least one entry + * This ensures providers properly document their version compatibility + +These validation rules help catch configuration issues early and provide clear error messages to assist in troubleshooting. +