sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/repository/repository_local.go (about) 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 "context" 21 "net/url" 22 "os" 23 "path/filepath" 24 "runtime" 25 "strings" 26 27 "github.com/pkg/errors" 28 "k8s.io/apimachinery/pkg/util/version" 29 30 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 31 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 32 ) 33 34 // localRepository provides support for providers located on the local filesystem. 35 // As part of the provider object, the URL is expected to contain the absolute 36 // path to the components yaml on the local filesystem. 37 // To support different versions, the directories containing provider 38 // specific data must adhere to the following layout: 39 // [file://]{basepath}/{provider-label}/{version}/{components.yaml} 40 // 41 // (1): {provider-label} must match the value returned by Provider.ManifestLabel() 42 // (2): {version} must obey the syntax and semantics of the "Semantic Versioning" 43 // specification (http://semver.org/); however, "latest" is also an acceptable value. 44 // 45 // Concrete example (linux): 46 // /home/user/go/src/sigs.k8s.io/infrastructure-aws/v0.4.7/infrastructure-components.yaml 47 // basepath: /home/user/go/src/sigs.k8s.io 48 // provider-label: infrastructure-aws 49 // version: v0.4.7 50 // components.yaml: infrastructure-components.yaml 51 // 52 // Concrete example (windows): 53 // NB. the input is an URI specification, not a windows path. see https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/ for more details 54 // /C:/cluster-api/out/repo/infrastructure-docker/latest/infrastructure-components.yaml 55 // basepath: C:\cluster-api\out\repo 56 // provider-label: infrastructure-docker 57 // version: v0.3.0 (whatever latest resolve to) 58 // components.yaml: infrastructure-components.yaml. 59 type localRepository struct { 60 providerConfig config.Provider 61 configVariablesClient config.VariablesClient 62 basepath string 63 providerLabel string 64 defaultVersion string 65 componentsPath string 66 } 67 68 var _ Repository = &localRepository{} 69 70 // DefaultVersion returns the default version for the local repository. 71 func (r *localRepository) DefaultVersion() string { 72 return r.defaultVersion 73 } 74 75 // RootPath returns the empty string as it is not applicable to local repositories. 76 func (r *localRepository) RootPath() string { 77 return "" 78 } 79 80 // ComponentsPath returns the path to the components file for the local repository. 81 func (r *localRepository) ComponentsPath() string { 82 return r.componentsPath 83 } 84 85 // GetFile returns a file for a given provider version. 86 func (r *localRepository) GetFile(ctx context.Context, version, fileName string) ([]byte, error) { 87 var err error 88 89 if version == latestVersionTag { 90 version, err = latestRelease(ctx, r) 91 if err != nil { 92 return nil, errors.Wrapf(err, "failed to get the latest release") 93 } 94 } else if version == "" { 95 version = r.defaultVersion 96 } 97 98 absolutePath := filepath.Join(r.basepath, r.providerLabel, version, r.RootPath(), fileName) 99 100 f, err := os.Stat(absolutePath) 101 if err != nil { 102 return nil, errors.Wrapf(err, "failed to read file %q from local release %s", absolutePath, version) 103 } 104 if f.IsDir() { 105 return nil, errors.Errorf("invalid path: file %q is actually a directory %q", fileName, absolutePath) 106 } 107 content, err := os.ReadFile(absolutePath) //nolint:gosec 108 if err != nil { 109 return nil, errors.Wrapf(err, "failed to read file %q from local release %s", absolutePath, version) 110 } 111 return content, nil 112 } 113 114 // GetVersions returns the list of versions that are available for a local repository. 115 func (r *localRepository) GetVersions(_ context.Context) ([]string, error) { 116 // get all the sub-directories under {basepath}/{provider-id}/ 117 releasesPath := filepath.Join(r.basepath, r.providerLabel) 118 files, err := os.ReadDir(releasesPath) 119 if err != nil { 120 return nil, errors.Wrap(err, "failed to list release directories") 121 } 122 versions := []string{} 123 for _, f := range files { 124 if !f.IsDir() { 125 continue 126 } 127 r := f.Name() 128 _, err := version.ParseSemantic(r) 129 if err != nil { 130 // discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases) 131 continue 132 } 133 versions = append(versions, r) 134 } 135 return versions, nil 136 } 137 138 // newLocalRepository returns a new localRepository. 139 func newLocalRepository(ctx context.Context, providerConfig config.Provider, configVariablesClient config.VariablesClient) (*localRepository, error) { 140 url, err := url.Parse(providerConfig.URL()) 141 if err != nil { 142 return nil, errors.Wrap(err, "invalid url") 143 } 144 145 // gets the path part of the url and check it is an absolute path 146 path := url.Path 147 if runtime.GOOS == "windows" { 148 // in case of windows, we should take care of removing the additional / which is required by the URI standard 149 // for windows local paths. see https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/ for more details. 150 // Encoded file paths are not required in Windows 10 versions <1803 and are unsupported in Windows 10 >=1803 151 // https://support.microsoft.com/en-us/help/4467268/url-encoded-unc-paths-not-url-decoded-in-windows-10-version-1803-later 152 path = filepath.FromSlash(strings.TrimPrefix(path, "/")) 153 } 154 if !filepath.IsAbs(path) { 155 return nil, errors.Errorf("invalid path: path %q must be an absolute path", providerConfig.URL()) 156 } 157 158 // Extracts provider-name, version, componentsPath from the url 159 // NB. format is {basepath}/{provider-name}/{version}/{components.yaml} 160 urlSplit := strings.Split(path, string(os.PathSeparator)) 161 if len(urlSplit) < 3 { 162 return nil, errors.Errorf("invalid path: path should be in the form {basepath}/{provider-name}/{version}/{components.yaml}") 163 } 164 165 componentsPath := urlSplit[len(urlSplit)-1] 166 defaultVersion := urlSplit[len(urlSplit)-2] 167 if defaultVersion != latestVersionTag { 168 _, err = version.ParseSemantic(defaultVersion) 169 if err != nil { 170 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) 171 } 172 } 173 providerID := urlSplit[len(urlSplit)-3] 174 if providerID != providerConfig.ManifestLabel() { 175 return nil, errors.Errorf("invalid path: path %q must contain provider %q in the format {basepath}/{provider-label}/{version}/{components.yaml}", providerConfig.URL(), providerConfig.ManifestLabel()) 176 } 177 178 // Get the base path, by trimming the last parts which are treated as a separated fields 179 var basePath string 180 basePath = strings.TrimSuffix(path, filepath.Join(providerID, defaultVersion, componentsPath)) 181 basePath = filepath.Clean(basePath) 182 183 repo := &localRepository{ 184 providerConfig: providerConfig, 185 configVariablesClient: configVariablesClient, 186 basepath: basePath, 187 providerLabel: providerID, 188 defaultVersion: defaultVersion, 189 componentsPath: componentsPath, 190 } 191 192 if defaultVersion == latestVersionTag { 193 repo.defaultVersion, err = latestContractRelease(ctx, repo, clusterv1.GroupVersion.Version) 194 if err != nil { 195 return nil, errors.Wrap(err, "failed to get latest version") 196 } 197 } 198 return repo, nil 199 }