sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/repository/repository_gitlab.go (about)

     1  /*
     2  Copyright 2022 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  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/pkg/errors"
    29  
    30  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
    31  )
    32  
    33  const (
    34  	gitlabHostPrefix          = "gitlab."
    35  	gitlabPackagesAPIPrefix   = "/api/v4/projects/"
    36  	gitlabPackagesAPIPackages = "packages"
    37  	gitlabPackagesAPIGeneric  = "generic"
    38  )
    39  
    40  // gitLabRepository provides support for providers hosted on GitLab.
    41  //
    42  // We support GitLab repositories that use the generic packages feature to publish artifacts and versions.
    43  // Repositories must use versioned releases.
    44  type gitLabRepository struct {
    45  	providerConfig        config.Provider
    46  	configVariablesClient config.VariablesClient
    47  	httpClient            *http.Client
    48  	host                  string
    49  	projectSlug           string
    50  	packageName           string
    51  	defaultVersion        string
    52  	rootPath              string
    53  	componentsPath        string
    54  }
    55  
    56  var _ Repository = &gitLabRepository{}
    57  
    58  // NewGitLabRepository returns a gitLabRepository implementation.
    59  func NewGitLabRepository(providerConfig config.Provider, configVariablesClient config.VariablesClient) (Repository, error) {
    60  	if configVariablesClient == nil {
    61  		return nil, errors.New("invalid arguments: configVariablesClient can't be nil")
    62  	}
    63  
    64  	rURL, err := url.Parse(providerConfig.URL())
    65  	if err != nil {
    66  		return nil, errors.Wrap(err, "invalid url")
    67  	}
    68  
    69  	urlSplit := strings.Split(strings.TrimPrefix(rURL.RawPath, "/"), "/")
    70  
    71  	// Check if the url is a Gitlab repository
    72  	if rURL.Scheme != httpsScheme ||
    73  		len(urlSplit) != 9 ||
    74  		!strings.HasPrefix(rURL.RawPath, gitlabPackagesAPIPrefix) ||
    75  		urlSplit[4] != gitlabPackagesAPIPackages ||
    76  		urlSplit[5] != gitlabPackagesAPIGeneric {
    77  		return nil, errors.New("invalid url: a GitLab repository url should be in the form https://{host}/api/v4/projects/{projectSlug}/packages/generic/{packageName}/{defaultVersion}/{componentsPath}")
    78  	}
    79  
    80  	httpClient := http.DefaultClient
    81  	// Extract all the info from url split.
    82  	host := rURL.Host
    83  	projectSlug := urlSplit[3]
    84  	packageName := urlSplit[6]
    85  	defaultVersion := urlSplit[7]
    86  	rootPath := "."
    87  	componentsPath := urlSplit[8]
    88  
    89  	repo := &gitLabRepository{
    90  		providerConfig:        providerConfig,
    91  		configVariablesClient: configVariablesClient,
    92  		httpClient:            httpClient,
    93  		host:                  host,
    94  		projectSlug:           projectSlug,
    95  		packageName:           packageName,
    96  		defaultVersion:        defaultVersion,
    97  		rootPath:              rootPath,
    98  		componentsPath:        componentsPath,
    99  	}
   100  
   101  	return repo, nil
   102  }
   103  
   104  // Host returns host field of gitLabRepository struct.
   105  func (g *gitLabRepository) Host() string {
   106  	return g.host
   107  }
   108  
   109  // ProjectSlug returns projectSlug field of gitLabRepository struct.
   110  func (g *gitLabRepository) ProjectSlug() string {
   111  	return g.projectSlug
   112  }
   113  
   114  // DefaultVersion returns defaultVersion field of gitLabRepository struct.
   115  func (g *gitLabRepository) DefaultVersion() string {
   116  	return g.defaultVersion
   117  }
   118  
   119  // GetVersions returns the list of versions that are available in a provider repository.
   120  func (g *gitLabRepository) GetVersions(_ context.Context) ([]string, error) {
   121  	// FIXME Get versions from GitLab API
   122  	return []string{g.defaultVersion}, nil
   123  }
   124  
   125  // RootPath returns rootPath field of gitLabRepository struct.
   126  func (g *gitLabRepository) RootPath() string {
   127  	return g.rootPath
   128  }
   129  
   130  // ComponentsPath returns componentsPath field of gitLabRepository struct.
   131  func (g *gitLabRepository) ComponentsPath() string {
   132  	return g.componentsPath
   133  }
   134  
   135  // GetFile returns a file for a given provider version.
   136  func (g *gitLabRepository) GetFile(ctx context.Context, version, path string) ([]byte, error) {
   137  	url := fmt.Sprintf(
   138  		"https://%s/api/v4/projects/%s/packages/generic/%s/%s/%s",
   139  		g.host,
   140  		g.projectSlug,
   141  		g.packageName,
   142  		version,
   143  		path,
   144  	)
   145  
   146  	if content, ok := cacheFiles[url]; ok {
   147  		return content, nil
   148  	}
   149  
   150  	timeoutctx, cancel := context.WithTimeout(ctx, 30*time.Second)
   151  	defer cancel()
   152  	request, err := http.NewRequestWithContext(timeoutctx, http.MethodGet, url, http.NoBody)
   153  	if err != nil {
   154  		return nil, errors.Wrapf(err, "failed to get file %q with version %q from %q: failed to create request", path, version, url)
   155  	}
   156  
   157  	response, err := g.httpClient.Do(request)
   158  	if err != nil {
   159  		return nil, errors.Wrapf(err, "failed to get file %q with version %q from %q", path, version, url)
   160  	}
   161  
   162  	defer response.Body.Close()
   163  
   164  	if response.StatusCode != http.StatusOK {
   165  		return nil, errors.Errorf("failed to get file %q with version %q from %q, got %d", path, version, url, response.StatusCode)
   166  	}
   167  
   168  	content, err := io.ReadAll(response.Body)
   169  	if err != nil {
   170  		return nil, errors.Wrapf(err, "failed to get file %q with version %q from %q", path, version, url)
   171  	}
   172  
   173  	cacheFiles[url] = content
   174  	return content, nil
   175  }