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  }