Skip to content

Commit b5b9f79

Browse files
committed
File system backend implementation for provider's repository.
1 parent 3117d18 commit b5b9f79

File tree

2 files changed

+584
-0
lines changed

2 files changed

+584
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
"net/url"
22+
"os"
23+
"path/filepath"
24+
"strings"
25+
26+
"github.com/pkg/errors"
27+
"k8s.io/apimachinery/pkg/util/version"
28+
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/config"
29+
)
30+
31+
// localRepository provides support for providers located on the local filesystem.
32+
// As part of the provider object, the URL is expected to contain the absolute
33+
// path to the components yaml on the local filesystem.
34+
// To support different versions, the directories containing provider
35+
// specific data must adhere to the following layout:
36+
// {basepath}/{provider-name}/{version}/{components.yaml}
37+
//
38+
// (1): {provider-name} must match the value returned by Provider.Name()
39+
// (2): {version} must obey the syntax and semantics of the "Semantic Versioning"
40+
// specification (http://semver.org/); however, "latest" is also an acceptable value.
41+
//
42+
// Concrete example:
43+
// /home/user/go/src/sigs.k8s.io/aws/v0.4.7/infrastructure-components.yaml
44+
// basepath: /home/user/go/src/sigs.k8s.io
45+
// provider-name: aws
46+
// version: v0.4.7
47+
// components.yaml: infrastructure-components.yaml
48+
type localRepository struct {
49+
providerConfig config.Provider
50+
configVariablesClient config.VariablesClient
51+
basepath string
52+
providerName string
53+
defaultVersion string
54+
componentsPath string
55+
}
56+
57+
var _ Repository = &localRepository{}
58+
59+
// DefaultVersion returns the default version for the local repository.
60+
func (r *localRepository) DefaultVersion() string {
61+
return r.defaultVersion
62+
}
63+
64+
// RootPath returns the empty string as it is not applicable to local repositories.
65+
func (r *localRepository) RootPath() string {
66+
return ""
67+
}
68+
69+
// ComponentsPath returns the path to the components file for the local repository.
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+
var err error
77+
78+
if version == "latest" {
79+
version, err = r.getLatestRelease()
80+
if err != nil {
81+
return nil, errors.Wrapf(err, "failed to get the latest release")
82+
}
83+
} else if version == "" {
84+
version = r.defaultVersion
85+
}
86+
87+
absolutePath := filepath.Join(r.basepath, r.providerName, version, r.RootPath(), fileName)
88+
89+
f, err := os.Stat(absolutePath)
90+
if err != nil {
91+
return nil, errors.Errorf("failed to read file %q from local release %s", absolutePath, version)
92+
}
93+
if f.IsDir() {
94+
return nil, errors.Errorf("invalid path: file %q is actually a directory %q", fileName, absolutePath)
95+
}
96+
content, err := ioutil.ReadFile(absolutePath)
97+
if err != nil {
98+
return nil, errors.Wrapf(err, "failed to read file %q from local release %s", absolutePath, version)
99+
}
100+
return content, nil
101+
102+
}
103+
104+
// GetVersions returns the list of versions that are available for a local repository.
105+
func (r *localRepository) GetVersions() ([]string, error) {
106+
// get all the sub-directories under {basepath}/{provider-name}/
107+
releasesPath := filepath.Join(r.basepath, r.providerName)
108+
files, err := ioutil.ReadDir(releasesPath)
109+
if err != nil {
110+
return nil, errors.Wrap(err, "failed to list release directories")
111+
}
112+
versions := []string{}
113+
for _, f := range files {
114+
if !f.IsDir() {
115+
continue
116+
}
117+
r := f.Name()
118+
sv, err := version.ParseSemantic(r)
119+
if err != nil {
120+
// discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases)
121+
continue
122+
}
123+
if sv.PreRelease() != "" || sv.BuildMetadata() != "" {
124+
// discard pre-releases or build releases (the user can point explicitly to such releases)
125+
continue
126+
}
127+
versions = append(versions, r)
128+
}
129+
return versions, nil
130+
}
131+
132+
// newLocalRepository returns a new localRepository.
133+
func newLocalRepository(providerConfig config.Provider, configVariablesClient config.VariablesClient) (*localRepository, error) {
134+
url, err := url.Parse(providerConfig.URL())
135+
if err != nil {
136+
return nil, errors.Wrap(err, "invalid url")
137+
}
138+
absPath := url.EscapedPath()
139+
if !filepath.IsAbs(absPath) {
140+
return nil, errors.Errorf("invalid path: path %q must be an absolute path", providerConfig.URL())
141+
}
142+
143+
urlSplit := strings.Split(providerConfig.URL(), "/")
144+
// {basepath}/{provider-name}/{version}/{components.yaml}
145+
if len(urlSplit) < 3 {
146+
return nil, errors.Errorf("invalid path: path should be in the form {basepath}/{provider-name}/{version}/{components.yaml}")
147+
}
148+
// We work our way backwards with {components.yaml} being the last part of the path
149+
componentsPath := urlSplit[len(urlSplit)-1]
150+
defaultVersion := urlSplit[len(urlSplit)-2]
151+
if defaultVersion != "latest" {
152+
_, err = version.ParseSemantic(defaultVersion)
153+
if err != nil {
154+
return nil, errors.Errorf("invalid version: %q. Version must obey the syntax and semantics of the \"Semantic Versioning\" specification (http://semver.org/) and path format {basepath}/{provider-name}/{version}/{components.yaml}", defaultVersion)
155+
}
156+
}
157+
providerName := urlSplit[len(urlSplit)-3]
158+
if providerName != providerConfig.Name() {
159+
return nil, errors.Errorf("invalid path: path %q must contain provider name %q in the format {basepath}/{provider-name}/{version}/{components.yaml}", providerConfig.URL(), providerConfig.Name())
160+
}
161+
var basePath string
162+
if len(urlSplit) > 3 {
163+
basePath = filepath.Join(urlSplit[:len(urlSplit)-3]...)
164+
}
165+
basePath = filepath.Clean("/" + basePath) // ensure basePath starts with "/"
166+
167+
repo := &localRepository{
168+
providerConfig: providerConfig,
169+
configVariablesClient: configVariablesClient,
170+
basepath: basePath,
171+
providerName: providerName,
172+
defaultVersion: defaultVersion,
173+
componentsPath: componentsPath,
174+
}
175+
176+
if defaultVersion == "latest" {
177+
repo.defaultVersion, err = repo.getLatestRelease()
178+
if err != nil {
179+
return nil, errors.Wrap(err, "failed to get latest version")
180+
}
181+
}
182+
return repo, nil
183+
}
184+
185+
// getLatestRelease returns the latest release for the local repository.
186+
func (r *localRepository) getLatestRelease() (string, error) {
187+
versions, err := r.GetVersions()
188+
if err != nil {
189+
return "", errors.Wrapf(err, "failed to get local repository versions")
190+
}
191+
var latestTag string
192+
var latestReleaseVersion *version.Version
193+
for _, v := range versions {
194+
sv, err := version.ParseSemantic(v)
195+
if err != nil {
196+
continue
197+
}
198+
if latestReleaseVersion == nil || latestReleaseVersion.LessThan(sv) {
199+
latestTag = v
200+
latestReleaseVersion = sv
201+
}
202+
}
203+
if latestTag == "" {
204+
return "", errors.New("failed to find releases tagged with a valid semantic version number")
205+
}
206+
return latestTag, nil
207+
}

0 commit comments

Comments
 (0)