Skip to content

Commit b79c1ff

Browse files
committed
File system backend implementation for provider's repository.
1 parent 43d71cd commit b79c1ff

File tree

2 files changed

+308
-0
lines changed

2 files changed

+308
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package repository
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/pkg/errors"
26+
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/config"
27+
)
28+
29+
// localRepository provides support for providers located on the local filesystem.
30+
// As part of the provider object, the URL is expected to contain the absolute
31+
// path to the components yaml on the local filesystem.
32+
// In order to support different versions, the directories containing provider
33+
// specific data must adhere to the following layout:
34+
// {basepath}/{owner}/{provider-name}/releases/{version}/{path/components.yaml}
35+
//
36+
// Concrete example:
37+
// /home/user/go/src/sigs.k8s.io/cluster-api-provider-aws/releases/v0.4.7/infrastructure-components.yaml
38+
// basepath: /home/user/go/src
39+
// owner : sigs.k8s.io
40+
// provider-name: cluster-api-provider-aws
41+
// version: v0.4.7
42+
// path/component.yaml: infrastructure-components.yaml
43+
//
44+
// NOTE: provider-name must match the value returned by Provider.Name()
45+
// since it is used to parse the above fields.
46+
type localRepository struct {
47+
providerConfig config.Provider
48+
configVariablesClient config.VariablesClient
49+
basepath string
50+
owner string
51+
providerName string
52+
defaultVersion string
53+
rootPath string
54+
componentsPath string
55+
}
56+
57+
var _ Repository = &localRepository{}
58+
59+
// DefaultVersion returns defaultVersion field of localRepository struct
60+
func (r *localRepository) DefaultVersion() string {
61+
return r.defaultVersion
62+
}
63+
64+
// RootPath returns rootPath field of localRepository struct
65+
func (r *localRepository) RootPath() string {
66+
return r.rootPath
67+
}
68+
69+
// ComponentsPath returns componentsPath field of localRepository struct
70+
func (r *localRepository) ComponentsPath() string {
71+
return r.componentsPath
72+
}
73+
74+
// GetFile returns a file for a given provider version
75+
func (r *localRepository) GetFile(version, fileName string) ([]byte, error) {
76+
77+
absolutePath := filepath.Join(r.basepath, r.owner, r.providerName, version, r.rootPath, fileName)
78+
79+
if f, err := os.Stat(absolutePath); err == nil {
80+
if f.IsDir() {
81+
return nil, errors.Errorf("invalid path: file %q is actually a directory %q", fileName, absolutePath)
82+
}
83+
content, err := ioutil.ReadFile(absolutePath)
84+
if err != nil {
85+
return nil, errors.Wrapf(err, "failed to read files from local release %s", version)
86+
}
87+
return content, nil
88+
}
89+
return nil, errors.Errorf("failed to read files from local release %s", version)
90+
91+
}
92+
93+
// newLocalRepository returns a localRepository implementation
94+
func newLocalRepository(providerConfig config.Provider, configVariablesClient config.VariablesClient) (*localRepository, error) {
95+
96+
var err error
97+
98+
if !filepath.IsAbs(providerConfig.URL()) {
99+
return nil, errors.Errorf("invalid path: path %q must be an absolute path", providerConfig.URL())
100+
}
101+
102+
parts := strings.Split(providerConfig.URL(), "/")
103+
// find the first occurance of provider name and use it as a seperator between base and layout
104+
providerNameIndex := 0
105+
for i, p := range parts {
106+
if p == providerConfig.Name() {
107+
providerNameIndex = i
108+
}
109+
}
110+
if providerNameIndex < 1 {
111+
return nil, errors.Errorf("invalid path: path %q must contain provider name %q", providerConfig.URL(), providerConfig.Name())
112+
}
113+
114+
basePath := filepath.Join(parts[:providerNameIndex-1]...)
115+
basePath = filepath.Clean("/" + basePath) // ensure basePath starts with "/"
116+
layoutPath := filepath.Join(parts[providerNameIndex-1:]...)
117+
layoutParts := strings.Split(layoutPath, "/")
118+
// Check if the layoutPath is in the expected format.
119+
if len(layoutParts) < 5 {
120+
return nil, errors.Errorf("invalid path: path should be in the form {basepath}/{owner}/{provider-name}/releases/{version}/{path/components.yaml}")
121+
}
122+
owner := layoutParts[0]
123+
providerName := layoutParts[1]
124+
defaultVersion := layoutParts[3]
125+
path := filepath.Join(layoutParts[4:]...)
126+
// rootpath is the relative path starting after {version} directory and excludes component fiename
127+
rootPath, componentsPath := filepath.Split(path)
128+
129+
repo := &localRepository{
130+
providerConfig: providerConfig,
131+
configVariablesClient: configVariablesClient,
132+
basepath: basePath,
133+
owner: owner,
134+
providerName: providerName,
135+
defaultVersion: defaultVersion,
136+
rootPath: rootPath,
137+
componentsPath: componentsPath,
138+
}
139+
140+
if defaultVersion == "latest" {
141+
repo.defaultVersion, err = repo.getLatestRelease()
142+
if err != nil {
143+
return nil, errors.Wrap(err, "failed to get latest version")
144+
}
145+
}
146+
147+
return repo, nil
148+
}
149+
150+
// getLatestRelease returns the latest release for the local repository.
151+
func (r *localRepository) getLatestRelease() (string, error) {
152+
// TODO
153+
return "0.0.0", nil
154+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package repository
18+
19+
import (
20+
"testing"
21+
22+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
23+
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/config"
24+
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/internal/test"
25+
)
26+
27+
func Test_localRepository_newLocalRepository(t *testing.T) {
28+
29+
type fields struct {
30+
provider config.Provider
31+
configVariablesClient config.VariablesClient
32+
}
33+
type want struct {
34+
basepath string
35+
owner string
36+
providerName string
37+
defaultVersion string
38+
rootPath string
39+
componentsPath string
40+
}
41+
tests := []struct {
42+
name string
43+
fields fields
44+
want want
45+
wantErr bool
46+
}{
47+
{
48+
name: "successfully creates new local repository object with a single version",
49+
fields: fields{
50+
provider: config.NewProvider("provider-foo", "/base/path/owner-foo/provider-foo/releases/v1.0.0/infrastructure-components.yaml", clusterctlv1.BootstrapProviderType),
51+
configVariablesClient: test.NewFakeVariableClient(),
52+
},
53+
want: want{
54+
basepath: "/base/path",
55+
owner: "owner-foo",
56+
providerName: "provider-foo",
57+
defaultVersion: "v1.0.0",
58+
rootPath: "",
59+
componentsPath: "infrastructure-components.yaml",
60+
},
61+
wantErr: false,
62+
},
63+
{
64+
name: "successfully creates new local repository object with a single version and no basepath",
65+
fields: fields{
66+
provider: config.NewProvider("provider-foo", "/owner-foo/provider-foo/releases/v1/infrastructure-components.yaml", clusterctlv1.BootstrapProviderType),
67+
configVariablesClient: test.NewFakeVariableClient(),
68+
},
69+
want: want{
70+
basepath: "/",
71+
owner: "owner-foo",
72+
providerName: "provider-foo",
73+
defaultVersion: "v1",
74+
rootPath: "",
75+
componentsPath: "infrastructure-components.yaml",
76+
},
77+
wantErr: false,
78+
},
79+
{
80+
name: "successfully creates new local repository object with a single version and a long rootpath",
81+
fields: fields{
82+
provider: config.NewProvider("provider-foo", "/owner-foo/provider-foo/releases/v1/foo/bar/infrastructure-components.yaml", clusterctlv1.BootstrapProviderType),
83+
configVariablesClient: test.NewFakeVariableClient(),
84+
},
85+
want: want{
86+
basepath: "/",
87+
owner: "owner-foo",
88+
providerName: "provider-foo",
89+
defaultVersion: "v1",
90+
rootPath: "foo/bar/",
91+
componentsPath: "infrastructure-components.yaml",
92+
},
93+
wantErr: false,
94+
},
95+
{
96+
name: "fails if an absolute path not specified",
97+
fields: fields{
98+
provider: config.NewProvider("provider-foo", "./owner-foo/provider-foo/releases/v1/foo/bar/infrastructure-components.yaml", clusterctlv1.BootstrapProviderType),
99+
configVariablesClient: test.NewFakeVariableClient(),
100+
},
101+
want: want{},
102+
wantErr: true,
103+
},
104+
{
105+
name: "fails if provider name does not match in the path",
106+
fields: fields{
107+
provider: config.NewProvider("provider-foo", "/foo/bar/owner-foo/provider-bar/releases/v1/infrastructure-components.yaml", clusterctlv1.BootstrapProviderType),
108+
configVariablesClient: test.NewFakeVariableClient(),
109+
},
110+
want: want{},
111+
wantErr: true,
112+
},
113+
{
114+
name: "fails if malformed path: no version directory",
115+
fields: fields{
116+
provider: config.NewProvider("provider-foo", "/foo/bar/owner-foo/provider-foo/releases/infrastructure-components.yaml", clusterctlv1.BootstrapProviderType),
117+
configVariablesClient: test.NewFakeVariableClient(),
118+
},
119+
want: want{},
120+
wantErr: true,
121+
},
122+
}
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
got, err := newLocalRepository(tt.fields.provider, tt.fields.configVariablesClient)
126+
if (err != nil) != tt.wantErr {
127+
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
128+
return
129+
}
130+
131+
if tt.wantErr {
132+
return
133+
}
134+
if got.basepath != tt.want.basepath {
135+
t.Errorf("basepath() got = %v, want = %v ", got.basepath, tt.want.basepath)
136+
}
137+
if got.owner != tt.want.owner {
138+
t.Errorf("owner() got = %v, want = %v ", got.owner, tt.want.owner)
139+
}
140+
if got.providerName != tt.want.providerName {
141+
t.Errorf("providerName() got = %v, want = %v ", got.providerName, tt.want.providerName)
142+
}
143+
if got.DefaultVersion() != tt.want.defaultVersion {
144+
t.Errorf("DefaultVersion() got = %v, want = %v ", got.DefaultVersion(), tt.want.defaultVersion)
145+
}
146+
if got.RootPath() != tt.want.rootPath {
147+
t.Errorf("RootPath() got = %v, want = %v ", got.RootPath(), tt.want.rootPath)
148+
}
149+
if got.ComponentsPath() != tt.want.componentsPath {
150+
t.Errorf("ComponentsPath() got = %v, want = %v ", got.ComponentsPath(), tt.want.componentsPath)
151+
}
152+
})
153+
}
154+
}

0 commit comments

Comments
 (0)