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 }