Skip to content

Commit db916ae

Browse files
✨clusterctl: validate provider metadata (#12242)
* ✨clusterctl: validate provider metadata and improve debug logs Adds stricter validation and richer diagnostics while reading provider metadata: * Introduce validateMetadata() to verify: * apiVersion matches clusterctl’s expected group/version * kind is “Metadata” * releaseSeries is not empty * Return clear, wrapped errors when decoding or validation fails. These changes help surface mis-shaped metadata early and make failures easier to troubleshoot. Signed-off-by: kahirokunn <okinakahiro@gmail.com> * Update cmd/clusterctl/client/repository/metadata_client.go Co-authored-by: Fabrizio Pandini <fabrizio.pandini@gmail.com> Signed-off-by: kahirokunn <okinakahiro@gmail.com> * 📚 docs: add validation rules documentation for provider metadata - Document metadata.yaml validation rules in clusterctl contract - Add clusterctl section to v1.10-to-v1.11 migration guide - Clarify required fields: apiVersion, kind, and releaseSeries Signed-off-by: kahirokunn <okinakahiro@gmail.com> --------- Signed-off-by: kahirokunn <okinakahiro@gmail.com> Co-authored-by: Fabrizio Pandini <fabrizio.pandini@gmail.com>
1 parent 495d454 commit db916ae

File tree

4 files changed

+136
-1
lines changed

4 files changed

+136
-1
lines changed

cmd/clusterctl/client/repository/metadata_client.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,36 @@ func (f *metadataClient) Get(ctx context.Context) (*clusterctlv1.Metadata, error
9292
return nil, errors.Wrapf(err, "error decoding %q for provider %q", metadataFile, f.provider.ManifestLabel())
9393
}
9494

95-
//TODO: consider if to add metadata validation (TBD)
95+
if err := validateMetadata(obj, f.provider.ManifestLabel()); err != nil {
96+
return nil, err
97+
}
9698

9799
return obj, nil
98100
}
101+
102+
// validateMetadata validates the metadata object structure.
103+
//
104+
// It checks if:
105+
// 1. The metadata has the correct apiVersion and kind.
106+
// 2. The metadata has at least one release series.
107+
//
108+
// Note: Version matching against releaseSeries is done later in `installer.go`.
109+
func validateMetadata(metadata *clusterctlv1.Metadata, providerLabel string) error {
110+
// Check if metadata has the correct apiVersion and kind
111+
if metadata.APIVersion != clusterctlv1.GroupVersion.String() {
112+
return errors.Errorf("invalid provider metadata: unexpected apiVersion %q for provider %s (expected %q)",
113+
metadata.APIVersion, providerLabel, clusterctlv1.GroupVersion.String())
114+
}
115+
116+
if metadata.Kind != "Metadata" {
117+
return errors.Errorf("invalid provider metadata: unexpected kind %q for provider %s (expected \"Metadata\")",
118+
metadata.Kind, providerLabel)
119+
}
120+
121+
// Check if metadata has at least one release series
122+
if len(metadata.ReleaseSeries) == 0 {
123+
return errors.Errorf("invalid provider metadata: releaseSeries is empty in metadata.yaml for provider %s", providerLabel)
124+
}
125+
126+
return nil
127+
}

cmd/clusterctl/client/repository/metadata_client_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,85 @@ func Test_metadataClient_Get(t *testing.T) {
135135
})
136136
}
137137
}
138+
139+
func Test_validateMetadata(t *testing.T) {
140+
tests := []struct {
141+
name string
142+
metadata *clusterctlv1.Metadata
143+
providerLabel string
144+
wantErr bool
145+
errMessage string
146+
}{
147+
{
148+
name: "valid metadata",
149+
metadata: &clusterctlv1.Metadata{
150+
TypeMeta: metav1.TypeMeta{
151+
APIVersion: clusterctlv1.GroupVersion.String(),
152+
Kind: "Metadata",
153+
},
154+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
155+
{Major: 1, Minor: 0, Contract: "v1beta1"},
156+
},
157+
},
158+
providerLabel: "infra-test",
159+
wantErr: false,
160+
},
161+
{
162+
name: "invalid apiVersion",
163+
metadata: &clusterctlv1.Metadata{
164+
TypeMeta: metav1.TypeMeta{
165+
APIVersion: "wrong.group/v1",
166+
Kind: "Metadata",
167+
},
168+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
169+
{Major: 1, Minor: 0, Contract: "v1beta1"},
170+
},
171+
},
172+
providerLabel: "infra-test",
173+
wantErr: true,
174+
errMessage: "invalid provider metadata: unexpected apiVersion \"wrong.group/v1\" for provider infra-test (expected \"clusterctl.cluster.x-k8s.io/v1alpha3\")",
175+
},
176+
{
177+
name: "invalid kind",
178+
metadata: &clusterctlv1.Metadata{
179+
TypeMeta: metav1.TypeMeta{
180+
APIVersion: clusterctlv1.GroupVersion.String(),
181+
Kind: "WrongKind",
182+
},
183+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
184+
{Major: 1, Minor: 0, Contract: "v1beta1"},
185+
},
186+
},
187+
providerLabel: "infra-test",
188+
wantErr: true,
189+
errMessage: "invalid provider metadata: unexpected kind \"WrongKind\" for provider infra-test (expected \"Metadata\")",
190+
},
191+
{
192+
name: "empty releaseSeries",
193+
metadata: &clusterctlv1.Metadata{
194+
TypeMeta: metav1.TypeMeta{
195+
APIVersion: clusterctlv1.GroupVersion.String(),
196+
Kind: "Metadata",
197+
},
198+
ReleaseSeries: []clusterctlv1.ReleaseSeries{},
199+
},
200+
providerLabel: "infra-test",
201+
wantErr: true,
202+
errMessage: "invalid provider metadata: releaseSeries is empty in metadata.yaml for provider infra-test",
203+
},
204+
}
205+
206+
for _, tt := range tests {
207+
t.Run(tt.name, func(t *testing.T) {
208+
g := NewWithT(t)
209+
210+
err := validateMetadata(tt.metadata, tt.providerLabel)
211+
if tt.wantErr {
212+
g.Expect(err).To(HaveOccurred())
213+
g.Expect(err.Error()).To(ContainSubstring(tt.errMessage))
214+
return
215+
}
216+
g.Expect(err).ToNot(HaveOccurred())
217+
})
218+
}
219+
}

docs/book/src/developer/providers/contracts/clusterctl.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,21 @@ releaseSeries:
201201
contract: v1alpha2
202202
```
203203

204+
#### Validation Rules
205+
206+
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:
207+
208+
1. **apiVersion**: Must be set to `clusterctl.cluster.x-k8s.io/v1alpha3`
209+
* This ensures compatibility with the current clusterctl metadata format
210+
211+
2. **kind**: Must be set to `Metadata`
212+
* This identifies the resource type correctly
213+
214+
3. **releaseSeries**: Must contain at least one entry
215+
* This ensures providers properly document their version compatibility
216+
217+
These validation rules help catch configuration issues early and provide clear error messages to assist in troubleshooting.
218+
204219
<aside class="note">
205220

206221
<h1> Note on user experience</h1>

docs/book/src/developer/providers/migrations/v1.10-to-v1.11.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,15 @@ TODO
380380

381381
TODO
382382

383+
### clusterctl
384+
385+
- **Stricter validation for provider metadata**: clusterctl now enforces validation rules when reading provider metadata files to ensure they are properly formatted and contain required information. The following validation rules are now enforced:
386+
- `apiVersion` must be set to `clusterctl.cluster.x-k8s.io/v1alpha3`
387+
- `kind` must be set to `Metadata`
388+
- `releaseSeries` must contain at least one entry
389+
390+
These changes help surface mis-shaped metadata early and make failures easier to troubleshoot. Providers with invalid metadata.yaml files will need to update them to comply with these validation rules.
391+
383392
## Deprecations
384393

385394
- v1beta1 API version is deprecated and it will be removed tentatively in August 2026

0 commit comments

Comments
 (0)