Skip to content

Commit 65a6617

Browse files
authored
Merge pull request #4866 from CecileRobertMichon/clusterctl-older-api-main
🐛 Use metadata.yaml in github repo to fetch latest release for older contracts
2 parents 355bb9e + cb6380b commit 65a6617

File tree

6 files changed

+300
-10
lines changed

6 files changed

+300
-10
lines changed

cmd/clusterctl/api/v1alpha3/metadata_type.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,14 @@ func (m *Metadata) GetReleaseSeriesForVersion(version *version.Version) *Release
6060

6161
return nil
6262
}
63+
64+
// GetReleaseSeriesForContract returns the release series for a given API Version, e.g. `v1alpha4`.
65+
func (m *Metadata) GetReleaseSeriesForContract(contract string) *ReleaseSeries {
66+
for _, releaseSeries := range m.ReleaseSeries {
67+
if contract == releaseSeries.Contract {
68+
return &releaseSeries
69+
}
70+
}
71+
72+
return nil
73+
}

cmd/clusterctl/client/cluster/installer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func (i *providerInstaller) getProviderContract(providerInstanceContracts map[st
174174
}
175175

176176
if releaseSeries.Contract != clusterv1.GroupVersion.Version {
177-
return "", errors.Errorf("current version of clusterctl could install only %s providers, detected %s for provider %s", clusterv1.GroupVersion.Version, releaseSeries.Contract, provider.ManifestLabel())
177+
return "", errors.Errorf("current version of clusterctl is only compatible with %s providers, detected %s for provider %s", clusterv1.GroupVersion.Version, releaseSeries.Contract, provider.ManifestLabel())
178178
}
179179

180180
providerInstanceContracts[provider.InstanceName()] = releaseSeries.Contract

cmd/clusterctl/client/repository/metadata_client.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
2727
)
2828

29+
const metadataFile = "metadata.yaml"
30+
2931
// MetadataClient has methods to work with metadata hosted on a provider repository.
3032
// Metadata are yaml files providing additional information about provider's assets like e.g the version compatibility Matrix.
3133
type MetadataClient interface {
@@ -59,33 +61,32 @@ func (f *metadataClient) Get() (*clusterctlv1.Metadata, error) {
5961

6062
// gets the metadata file from the repository
6163
version := f.version
62-
name := "metadata.yaml"
6364

6465
file, err := getLocalOverride(&newOverrideInput{
6566
configVariablesClient: f.configVarClient,
6667
provider: f.provider,
6768
version: version,
68-
filePath: name,
69+
filePath: metadataFile,
6970
})
7071
if err != nil {
7172
return nil, err
7273
}
7374
if file == nil {
74-
log.V(5).Info("Fetching", "File", name, "Provider", f.provider.Name(), "Type", f.provider.Type(), "Version", version)
75-
file, err = f.repository.GetFile(version, name)
75+
log.V(5).Info("Fetching", "File", metadataFile, "Provider", f.provider.Name(), "Type", f.provider.Type(), "Version", version)
76+
file, err = f.repository.GetFile(version, metadataFile)
7677
if err != nil {
77-
return nil, errors.Wrapf(err, "failed to read %q from the repository for provider %q", name, f.provider.ManifestLabel())
78+
return nil, errors.Wrapf(err, "failed to read %q from the repository for provider %q", metadataFile, f.provider.ManifestLabel())
7879
}
7980
} else {
80-
log.V(1).Info("Using", "Override", name, "Provider", f.provider.ManifestLabel(), "Version", version)
81+
log.V(1).Info("Using", "Override", metadataFile, "Provider", f.provider.ManifestLabel(), "Version", version)
8182
}
8283

8384
// Convert the yaml into a typed object
8485
obj := &clusterctlv1.Metadata{}
8586
codecFactory := serializer.NewCodecFactory(scheme.Scheme)
8687

8788
if err := runtime.DecodeInto(codecFactory.UniversalDecoder(), file, obj); err != nil {
88-
return nil, errors.Wrapf(err, "error decoding %q for provider %q", name, f.provider.ManifestLabel())
89+
return nil, errors.Wrapf(err, "error decoding %q for provider %q", metadataFile, f.provider.ManifestLabel())
8990
}
9091

9192
//TODO: consider if to add metadata validation (TBD)

cmd/clusterctl/client/repository/repository_github.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ import (
2525
"path/filepath"
2626
"strings"
2727

28+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
29+
30+
"k8s.io/apimachinery/pkg/runtime"
31+
"k8s.io/apimachinery/pkg/runtime/serializer"
32+
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
33+
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
34+
2835
"github.com/google/go-github/v33/github"
2936
"github.com/pkg/errors"
3037
"golang.org/x/oauth2"
@@ -171,7 +178,7 @@ func newGitHubRepository(providerConfig config.Provider, configVariablesClient c
171178
}
172179

173180
if defaultVersion == githubLatestReleaseLabel {
174-
repo.defaultVersion, err = repo.getLatestRelease()
181+
repo.defaultVersion, err = repo.getLatestContractRelease(clusterv1.GroupVersion.Version)
175182
if err != nil {
176183
return nil, errors.Wrap(err, "failed to get GitHub latest version")
177184
}
@@ -238,9 +245,53 @@ func (g *gitHubRepository) getVersions() ([]string, error) {
238245
return versions, nil
239246
}
240247

248+
// getLatestContractRelease returns the latest patch release for a github repository for the current API contract, according to
249+
// semantic version order of the release tag name.
250+
func (g *gitHubRepository) getLatestContractRelease(contract string) (string, error) {
251+
latest, err := g.getLatestRelease()
252+
if err != nil {
253+
return latest, err
254+
}
255+
// Attempt to check if the latest release satisfies the API Contract
256+
// This is a best-effort attempt to find the latest release for an older API contract if it's not the latest Github release.
257+
// If an error occurs, we just return the latest release.
258+
file, err := g.GetFile(latest, metadataFile)
259+
if err != nil {
260+
// if we can't get the metadata file from the release, we return latest.
261+
return latest, nil // nolint:nilerr
262+
}
263+
latestMetadata := &clusterctlv1.Metadata{}
264+
codecFactory := serializer.NewCodecFactory(scheme.Scheme)
265+
if err := runtime.DecodeInto(codecFactory.UniversalDecoder(), file, latestMetadata); err != nil {
266+
return latest, nil // nolint:nilerr
267+
}
268+
269+
releaseSeries := latestMetadata.GetReleaseSeriesForContract(contract)
270+
if releaseSeries == nil {
271+
return latest, nil
272+
}
273+
274+
sv, err := version.ParseSemantic(latest)
275+
if err != nil {
276+
return latest, nil // nolint:nilerr
277+
}
278+
279+
// If the Major or Minor version of the latest release doesn't match the release series for the current contract,
280+
// return the latest patch release of the desired Major/Minor version.
281+
if sv.Major() != releaseSeries.Major || sv.Minor() != releaseSeries.Minor {
282+
return g.getLatestPatchRelease(&releaseSeries.Major, &releaseSeries.Minor)
283+
}
284+
return latest, nil
285+
}
286+
241287
// getLatestRelease returns the latest release for a github repository, according to
242288
// semantic version order of the release tag name.
243289
func (g *gitHubRepository) getLatestRelease() (string, error) {
290+
return g.getLatestPatchRelease(nil, nil)
291+
}
292+
293+
// getLatestRelease returns the latest patch release for a given Major and Minor version.
294+
func (g *gitHubRepository) getLatestPatchRelease(major, minor *uint) (string, error) {
244295
versions, err := g.getVersions()
245296
if err != nil {
246297
return "", g.handleGithubErr(err, "failed to get the list of versions")
@@ -261,6 +312,11 @@ func (g *gitHubRepository) getLatestRelease() (string, error) {
261312
continue
262313
}
263314

315+
if (major != nil && sv.Major() != *major) || (minor != nil && sv.Minor() != *minor) {
316+
// skip versions that don't match the desired Major.Minor version.
317+
continue
318+
}
319+
264320
// track prereleases separately
265321
if sv.PreRelease() != "" {
266322
if latestPrereleaseVersion == nil || latestPrereleaseVersion.LessThan(sv) {

cmd/clusterctl/client/repository/repository_github_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,93 @@ func Test_gitHubRepository_getVersions(t *testing.T) {
283283
}
284284
}
285285

286+
func Test_gitHubRepository_getLatestContractRelease(t *testing.T) {
287+
client, mux, teardown := test.NewFakeGitHub()
288+
defer teardown()
289+
290+
// setup an handler for returning 3 fake releases
291+
mux.HandleFunc("/repos/o/r1/releases", func(w http.ResponseWriter, r *http.Request) {
292+
testMethod(t, r, "GET")
293+
fmt.Fprint(w, `[`)
294+
fmt.Fprint(w, `{"id":1, "tag_name": "v0.4.0", "assets": [{"id": 1, "name": "metadata.yaml"}]},`)
295+
fmt.Fprint(w, `{"id":2, "tag_name": "v0.3.2", "assets": [{"id": 1, "name": "metadata.yaml"}]},`)
296+
fmt.Fprint(w, `{"id":3, "tag_name": "v0.3.1", "assets": [{"id": 1, "name": "metadata.yaml"}]}`)
297+
fmt.Fprint(w, `]`)
298+
})
299+
300+
// test.NewFakeGitHub and handler for returning a fake release
301+
mux.HandleFunc("/repos/o/r1/releases/tags/v0.4.0", func(w http.ResponseWriter, r *http.Request) {
302+
testMethod(t, r, "GET")
303+
fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.0", "assets": [{"id": 1, "name": "metadata.yaml"}] }`)
304+
})
305+
306+
// test.NewFakeGitHub an handler for returning a fake release metadata file
307+
mux.HandleFunc("/repos/o/r1/releases/assets/1", func(w http.ResponseWriter, r *http.Request) {
308+
testMethod(t, r, "GET")
309+
w.Header().Set("Content-Type", "application/octet-stream")
310+
w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml")
311+
fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n")
312+
})
313+
314+
configVariablesClient := test.NewFakeVariableClient()
315+
316+
type field struct {
317+
providerConfig config.Provider
318+
}
319+
tests := []struct {
320+
name string
321+
field field
322+
contract string
323+
want string
324+
wantErr bool
325+
}{
326+
{
327+
name: "Get latest release if it matches the contract",
328+
field: field{
329+
providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
330+
},
331+
contract: "v1alpha4",
332+
want: "v0.4.0",
333+
wantErr: false,
334+
},
335+
{
336+
name: "Get previous release if the latest doesn't match the contract",
337+
field: field{
338+
providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
339+
},
340+
contract: "v1alpha3",
341+
want: "v0.3.2",
342+
wantErr: false,
343+
},
344+
{
345+
name: "Return the latest release if the contract doesn't exist",
346+
field: field{
347+
providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
348+
},
349+
want: "v0.4.0",
350+
contract: "foo",
351+
wantErr: false,
352+
},
353+
}
354+
for _, tt := range tests {
355+
t.Run(tt.name, func(t *testing.T) {
356+
g := NewWithT(t)
357+
resetCaches()
358+
359+
gRepo, err := newGitHubRepository(tt.field.providerConfig, configVariablesClient, injectGithubClient(client))
360+
g.Expect(err).NotTo(HaveOccurred())
361+
362+
got, err := gRepo.getLatestContractRelease(tt.contract)
363+
if tt.wantErr {
364+
g.Expect(err).To(HaveOccurred())
365+
return
366+
}
367+
g.Expect(err).NotTo(HaveOccurred())
368+
g.Expect(got).To(Equal(tt.want))
369+
})
370+
}
371+
}
372+
286373
func Test_gitHubRepository_getLatestRelease(t *testing.T) {
287374
client, mux, teardown := test.NewFakeGitHub()
288375
defer teardown()
@@ -370,6 +457,87 @@ func Test_gitHubRepository_getLatestRelease(t *testing.T) {
370457
}
371458
}
372459

460+
func Test_gitHubRepository_getLatestPatchRelease(t *testing.T) {
461+
client, mux, teardown := test.NewFakeGitHub()
462+
defer teardown()
463+
464+
// setup an handler for returning 3 fake releases
465+
mux.HandleFunc("/repos/o/r1/releases", func(w http.ResponseWriter, r *http.Request) {
466+
testMethod(t, r, "GET")
467+
fmt.Fprint(w, `[`)
468+
fmt.Fprint(w, `{"id":1, "tag_name": "v0.4.0"},`)
469+
fmt.Fprint(w, `{"id":2, "tag_name": "v0.3.2"},`)
470+
fmt.Fprint(w, `{"id":3, "tag_name": "v1.3.2"}`)
471+
fmt.Fprint(w, `]`)
472+
})
473+
474+
major0 := uint(0)
475+
minor3 := uint(3)
476+
minor4 := uint(4)
477+
478+
configVariablesClient := test.NewFakeVariableClient()
479+
480+
type field struct {
481+
providerConfig config.Provider
482+
}
483+
tests := []struct {
484+
name string
485+
field field
486+
major *uint
487+
minor *uint
488+
want string
489+
wantErr bool
490+
}{
491+
{
492+
name: "Get latest patch release, no Major/Minor specified",
493+
field: field{
494+
providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
495+
},
496+
minor: nil,
497+
major: nil,
498+
want: "v1.3.2",
499+
wantErr: false,
500+
},
501+
{
502+
name: "Get latest patch release, for Major 0 and Minor 3",
503+
field: field{
504+
providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
505+
},
506+
major: &major0,
507+
minor: &minor3,
508+
want: "v0.3.2",
509+
wantErr: false,
510+
},
511+
{
512+
name: "Get latest patch release, for Major 0 and Minor 4",
513+
field: field{
514+
providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
515+
},
516+
major: &major0,
517+
minor: &minor4,
518+
want: "v0.4.0",
519+
wantErr: false,
520+
},
521+
}
522+
for _, tt := range tests {
523+
t.Run(tt.name, func(t *testing.T) {
524+
g := NewWithT(t)
525+
resetCaches()
526+
527+
gRepo, err := newGitHubRepository(tt.field.providerConfig, configVariablesClient, injectGithubClient(client))
528+
g.Expect(err).NotTo(HaveOccurred())
529+
530+
got, err := gRepo.getLatestPatchRelease(tt.major, tt.minor)
531+
if tt.wantErr {
532+
g.Expect(err).To(HaveOccurred())
533+
return
534+
}
535+
g.Expect(err).NotTo(HaveOccurred())
536+
g.Expect(got).To(Equal(tt.want))
537+
})
538+
}
539+
}
540+
373541
func Test_gitHubRepository_getReleaseByTag(t *testing.T) {
374542
client, mux, teardown := test.NewFakeGitHub()
375543
defer teardown()

0 commit comments

Comments
 (0)