Skip to content

Commit 292bc12

Browse files
committed
Added getLatestRelease() to fetch the latest release.
Added unit-tests for latest release logic.
1 parent 47a45c4 commit 292bc12

File tree

2 files changed

+242
-8
lines changed

2 files changed

+242
-8
lines changed

cmd/clusterctl/pkg/client/repository/repository_local_unix.go

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package repository
1818

1919
import (
2020
"io/ioutil"
21+
"net/url"
2122
"os"
2223
"path/filepath"
2324
"strings"
2425

2526
"github.com/pkg/errors"
27+
"k8s.io/apimachinery/pkg/util/version"
2628
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/config"
2729
)
2830

@@ -33,6 +35,10 @@ import (
3335
// specific data must adhere to the following layout:
3436
// {basepath}/{owner}/{provider-name}/releases/{version}/{path/components.yaml}
3537
//
38+
// (1): {provider-name} must match the value returned by Provider.Name()
39+
// (2): {version} must exactly obey the syntax and semantics of
40+
// the "Semantic Versioning" specification (http://semver.org/).
41+
//
3642
// Concrete example:
3743
// /home/user/go/src/sigs.k8s.io/cluster-api-provider-aws/releases/v0.4.7/infrastructure-components.yaml
3844
// basepath: /home/user/go/src
@@ -41,8 +47,7 @@ import (
4147
// version: v0.4.7
4248
// path/component.yaml: infrastructure-components.yaml
4349
//
44-
// NOTE: provider-name must match the value returned by Provider.Name()
45-
// since it is used to parse the above fields.
50+
4651
type localRepository struct {
4752
providerConfig config.Provider
4853
configVariablesClient config.VariablesClient
@@ -73,8 +78,18 @@ func (r *localRepository) ComponentsPath() string {
7378

7479
// GetFile returns a file for a given provider version
7580
func (r *localRepository) GetFile(version, fileName string) ([]byte, error) {
81+
var err error
82+
83+
if version == "latest" {
84+
version, err = r.getLatestRelease()
85+
if err != nil {
86+
return nil, errors.Wrapf(err, "failed to get the latest release")
87+
}
88+
} else if version == "" {
89+
version = r.defaultVersion
90+
}
7691

77-
absolutePath := filepath.Join(r.basepath, r.owner, r.providerName, version, r.rootPath, fileName)
92+
absolutePath := filepath.Join(r.basepath, r.owner, r.providerName, "releases", version, r.rootPath, fileName)
7893

7994
if f, err := os.Stat(absolutePath); err == nil {
8095
if f.IsDir() {
@@ -86,7 +101,7 @@ func (r *localRepository) GetFile(version, fileName string) ([]byte, error) {
86101
}
87102
return content, nil
88103
}
89-
return nil, errors.Errorf("failed to read files from local release %s", version)
104+
return nil, errors.Errorf("failed to read files from local release %s, path %s", version, absolutePath)
90105

91106
}
92107

@@ -95,7 +110,16 @@ func newLocalRepository(providerConfig config.Provider, configVariablesClient co
95110

96111
var err error
97112

98-
if !filepath.IsAbs(providerConfig.URL()) {
113+
u, err := url.Parse(providerConfig.URL())
114+
if err != nil {
115+
return nil, errors.Wrap(err, "invalid url")
116+
}
117+
absPath := u.Path
118+
if u.RawPath != "" {
119+
absPath = u.RawPath
120+
}
121+
122+
if !filepath.IsAbs(absPath) {
99123
return nil, errors.Errorf("invalid path: path %q must be an absolute path", providerConfig.URL())
100124
}
101125

@@ -149,6 +173,43 @@ func newLocalRepository(providerConfig config.Provider, configVariablesClient co
149173

150174
// getLatestRelease returns the latest release for the local repository.
151175
func (r *localRepository) getLatestRelease() (string, error) {
152-
// TODO
153-
return "0.0.0", nil
176+
177+
// get all the sub-directories under {releases} directory
178+
releasesPath := filepath.Join(r.basepath, r.owner, r.providerName, "releases")
179+
files, err := ioutil.ReadDir(releasesPath)
180+
if err != nil {
181+
return "", errors.Wrap(err, "failed to list release directories")
182+
}
183+
var releases []string
184+
for _, f := range files {
185+
if f.IsDir() {
186+
releases = append(releases, f.Name())
187+
}
188+
}
189+
190+
// search for the latest release according to semantic version
191+
// releases with names that are not semantic version number are ignored
192+
var latestTag string
193+
var latestReleaseVersion *version.Version
194+
for _, r := range releases {
195+
sv, err := version.ParseSemantic(r)
196+
if err != nil {
197+
// discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases)
198+
continue
199+
}
200+
if sv.PreRelease() != "" || sv.BuildMetadata() != "" {
201+
// discard pre-releases or build releases (the user can point explicitly to such releases)
202+
continue
203+
}
204+
if latestReleaseVersion == nil || latestReleaseVersion.LessThan(sv) {
205+
latestTag = r
206+
latestReleaseVersion = sv
207+
}
208+
}
209+
210+
if latestTag == "" {
211+
return "", errors.New("failed to find releases tagged with a valid semantic version number")
212+
}
213+
214+
return latestTag, nil
154215
}

cmd/clusterctl/pkg/client/repository/repository_local_unix_test.go

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ limitations under the License.
1717
package repository
1818

1919
import (
20+
"io/ioutil"
21+
"os"
22+
"path/filepath"
2023
"testing"
2124

2225
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
@@ -124,7 +127,7 @@ func Test_localRepository_newLocalRepository(t *testing.T) {
124127
t.Run(tt.name, func(t *testing.T) {
125128
got, err := newLocalRepository(tt.fields.provider, tt.fields.configVariablesClient)
126129
if (err != nil) != tt.wantErr {
127-
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
130+
t.Errorf("newLocalRepository() error = %v, wantErr %v", err, tt.wantErr)
128131
return
129132
}
130133

@@ -152,3 +155,173 @@ func Test_localRepository_newLocalRepository(t *testing.T) {
152155
})
153156
}
154157
}
158+
159+
func tempDir(t *testing.T) string {
160+
dir, err := ioutil.TempDir("", "cc")
161+
if err != nil {
162+
t.Fatalf("err: %s", err)
163+
}
164+
if err := os.RemoveAll(dir); err != nil {
165+
t.Fatalf("err: %s", err)
166+
}
167+
168+
return dir
169+
}
170+
171+
func tempTestLayout(t *testing.T, tmpDir, path, msg string) string {
172+
173+
dst := filepath.Join(tmpDir, path)
174+
// Create all directories in the standard layout
175+
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
176+
t.Fatalf("err: %s", err)
177+
}
178+
179+
err := ioutil.WriteFile(dst, []byte(msg), 0644)
180+
if err != nil {
181+
t.Fatalf("err: %s", err)
182+
}
183+
return dst
184+
}
185+
186+
func Test_localRepository_newLocalRepository_Latest(t *testing.T) {
187+
tmpDir := tempDir(t)
188+
defer os.RemoveAll(filepath.Dir(tmpDir))
189+
190+
// Create several release directories
191+
tempTestLayout(t, tmpDir, "owner-foo/provider-2/releases/v1.0.0/infrastructure-components.yaml", "foo: bar")
192+
tempTestLayout(t, tmpDir, "owner-foo/provider-2/releases/v1.0.1/infrastructure-components.yaml", "foo: bar")
193+
tempTestLayout(t, tmpDir, "owner-foo/provider-2/releases/Foo.Bar/infrastructure-components.yaml", "foo: bar")
194+
// Provider URL for the latest release
195+
p2URLLatest := "owner-foo/provider-2/releases/latest/infrastructure-components.yaml"
196+
p2URLLatestAbs := filepath.Join(tmpDir, p2URLLatest)
197+
p2 := config.NewProvider("provider-2", p2URLLatestAbs, clusterctlv1.BootstrapProviderType)
198+
199+
t.Run("Pass: select latest release from multiple release directories", func(t *testing.T) {
200+
got, err := newLocalRepository(p2, test.NewFakeVariableClient())
201+
if err != nil {
202+
t.Errorf("newLocalRepository() got error %v", err)
203+
return
204+
}
205+
206+
if got.basepath != tmpDir {
207+
t.Errorf("basepath() got = %v, want = %v ", got.basepath, tmpDir)
208+
}
209+
if got.owner != "owner-foo" {
210+
t.Errorf("owner() got = %v, want = owner-foo ", got.owner)
211+
}
212+
if got.providerName != "provider-2" {
213+
t.Errorf("providerName() got = %v, want = provider-2 ", got.providerName)
214+
}
215+
if got.DefaultVersion() != "v1.0.1" {
216+
t.Errorf("DefaultVersion() got = %v, want = v1.0.1 ", got.DefaultVersion())
217+
}
218+
if got.RootPath() != "" {
219+
t.Errorf("RootPath() got = %v, want = \"\" ", got.RootPath())
220+
}
221+
if got.ComponentsPath() != "infrastructure-components.yaml" {
222+
t.Errorf("ComponentsPath() got = %v, want = infrastructure-components.yaml ", got.ComponentsPath())
223+
}
224+
})
225+
}
226+
227+
func Test_localRepository_GetFile(t *testing.T) {
228+
tmpDir := tempDir(t)
229+
defer os.RemoveAll(filepath.Dir(tmpDir))
230+
231+
// Provider 1: URL is for the only release available
232+
dst1 := tempTestLayout(t, tmpDir, "owner-foo/provider-1/releases/v1.0.0/infrastructure-components.yaml", "foo: bar")
233+
p1 := config.NewProvider("provider-1", dst1, clusterctlv1.BootstrapProviderType)
234+
235+
// Provider 2: URL is for the latest release
236+
tempTestLayout(t, tmpDir, "owner-foo/provider-2/releases/v1.0.0/infrastructure-components.yaml", "version: v1.0.0")
237+
tempTestLayout(t, tmpDir, "owner-foo/provider-2/releases/v1.0.1/infrastructure-components.yaml", "version: v1.0.1")
238+
tempTestLayout(t, tmpDir, "owner-foo/provider-2/releases/Foo.Bar/infrastructure-components.yaml", "version: Foo.Bar")
239+
p2URLLatest := "owner-foo/provider-2/releases/latest/infrastructure-components.yaml"
240+
p2URLLatestAbs := filepath.Join(tmpDir, p2URLLatest)
241+
p2 := config.NewProvider("provider-2", p2URLLatestAbs, clusterctlv1.BootstrapProviderType)
242+
243+
type fields struct {
244+
provider config.Provider
245+
configVariablesClient config.VariablesClient
246+
}
247+
type args struct {
248+
version string
249+
fileName string
250+
}
251+
type want struct {
252+
contents string
253+
}
254+
tests := []struct {
255+
name string
256+
fields fields
257+
args args
258+
want want
259+
wantErr bool
260+
}{
261+
{
262+
name: "Get file from release directory",
263+
fields: fields{
264+
provider: p1,
265+
configVariablesClient: test.NewFakeVariableClient(),
266+
},
267+
args: args{
268+
version: "v1.0.0",
269+
fileName: "infrastructure-components.yaml",
270+
},
271+
want: want{
272+
contents: "foo: bar",
273+
},
274+
wantErr: false,
275+
},
276+
{
277+
name: "Get file from latest release directory",
278+
fields: fields{
279+
provider: p2,
280+
configVariablesClient: test.NewFakeVariableClient(),
281+
},
282+
args: args{
283+
version: "latest",
284+
fileName: "infrastructure-components.yaml",
285+
},
286+
want: want{
287+
contents: "version: v1.0.1", // We use the file contents to determine data was read from latest release
288+
},
289+
wantErr: false,
290+
},
291+
{
292+
name: "Get file from default version release directory",
293+
fields: fields{
294+
provider: p2,
295+
configVariablesClient: test.NewFakeVariableClient(),
296+
},
297+
args: args{
298+
version: "",
299+
fileName: "infrastructure-components.yaml",
300+
},
301+
want: want{
302+
contents: "version: v1.0.1", // We use the file contents to determine data was read from latest release
303+
},
304+
wantErr: false,
305+
},
306+
}
307+
for _, tt := range tests {
308+
t.Run(tt.name, func(t *testing.T) {
309+
r, err := newLocalRepository(tt.fields.provider, tt.fields.configVariablesClient)
310+
if err != nil {
311+
t.Errorf("newLocalRepository() threw unexpected error: %v", err)
312+
return
313+
}
314+
got, err := r.GetFile(tt.args.version, tt.args.fileName)
315+
if (err != nil) != tt.wantErr {
316+
t.Errorf("GetFile() error = %v, wantErr %v", err, tt.wantErr)
317+
return
318+
}
319+
if tt.wantErr {
320+
return
321+
}
322+
if string(got) != tt.want.contents {
323+
t.Errorf("GetFile() returned %s expected %s", got, tt.want.contents)
324+
}
325+
})
326+
}
327+
}

0 commit comments

Comments
 (0)