github.com/oam-dev/kubevela@v1.9.11/pkg/addon/source.go (about) 1 /* 2 Copyright 2021 The KubeVela 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 addon 18 19 import ( 20 "fmt" 21 "net/url" 22 "path" 23 "strings" 24 25 "github.com/go-resty/resty/v2" 26 "github.com/pkg/errors" 27 "github.com/xanzy/go-gitlab" 28 29 "github.com/oam-dev/kubevela/pkg/utils" 30 ) 31 32 const ( 33 // EOFError is error returned by xml parse 34 EOFError string = "EOF" 35 // DirType means a directory 36 DirType = "dir" 37 // FileType means a file 38 FileType = "file" 39 // BlobType means a blob 40 BlobType = "blob" 41 // TreeType means a tree 42 TreeType = "tree" 43 44 bucketTmpl = "%s://%s.%s" 45 singleOSSFileTmpl = "%s/%s" 46 listOSSFileTmpl = "%s?max-keys=1000&prefix=%s" 47 ) 48 49 // Source is where to get addons, Registry implement this interface 50 type Source interface { 51 GetUIData(meta *SourceMeta, opt ListOptions) (*UIData, error) 52 ListUIData(registryAddonMeta map[string]SourceMeta, opt ListOptions) ([]*UIData, error) 53 GetInstallPackage(meta *SourceMeta, uiData *UIData) (*InstallPackage, error) 54 ListAddonMeta() (map[string]SourceMeta, error) 55 } 56 57 // GitAddonSource defines the information about the Git as addon source 58 type GitAddonSource struct { 59 URL string `json:"url,omitempty" validate:"required"` 60 Path string `json:"path,omitempty"` 61 Token string `json:"token,omitempty"` 62 } 63 64 // GiteeAddonSource defines the information about the Gitee as addon source 65 type GiteeAddonSource struct { 66 URL string `json:"url,omitempty" validate:"required"` 67 Path string `json:"path,omitempty"` 68 Token string `json:"token,omitempty"` 69 } 70 71 // GitlabAddonSource defines the information about the Gitlab as addon source 72 type GitlabAddonSource struct { 73 URL string `json:"url,omitempty" validate:"required"` 74 Repo string `json:"repo,omitempty" validate:"required"` 75 Path string `json:"path,omitempty"` 76 Token string `json:"token,omitempty"` 77 } 78 79 // HelmSource defines the information about the helm repo addon source 80 type HelmSource struct { 81 URL string `json:"url,omitempty" validate:"required"` 82 InsecureSkipTLS bool `json:"insecureSkipTLS,omitempty"` 83 Username string `json:"username,omitempty"` 84 Password string `json:"password,omitempty"` 85 } 86 87 // SafeCopier is an interface to copy Struct without sensitive fields, such as Token, Username, Password 88 type SafeCopier interface { 89 SafeCopy() interface{} 90 } 91 92 // SafeCopy hides field Token 93 func (g *GitAddonSource) SafeCopy() *GitAddonSource { 94 if g == nil { 95 return nil 96 } 97 return &GitAddonSource{ 98 URL: g.URL, 99 Path: g.Path, 100 } 101 } 102 103 // SafeCopy hides field Token 104 func (g *GiteeAddonSource) SafeCopy() *GiteeAddonSource { 105 if g == nil { 106 return nil 107 } 108 return &GiteeAddonSource{ 109 URL: g.URL, 110 Path: g.Path, 111 } 112 } 113 114 // SafeCopy hides field Token 115 func (g *GitlabAddonSource) SafeCopy() *GitlabAddonSource { 116 if g == nil { 117 return nil 118 } 119 return &GitlabAddonSource{ 120 URL: g.URL, 121 Repo: g.Repo, 122 Path: g.Path, 123 } 124 } 125 126 // SafeCopy hides field Username, Password 127 func (h *HelmSource) SafeCopy() *HelmSource { 128 if h == nil { 129 return nil 130 } 131 return &HelmSource{ 132 URL: h.URL, 133 } 134 } 135 136 // Item is a partial interface for github.RepositoryContent 137 type Item interface { 138 // GetType return "dir" or "file" 139 GetType() string 140 GetPath() string 141 GetName() string 142 } 143 144 // SourceMeta record the whole metadata of an addon 145 type SourceMeta struct { 146 Name string 147 Items []Item 148 } 149 150 // ClassifyItemByPattern will filter and classify addon data, data will be classified by pattern it meets 151 func ClassifyItemByPattern(meta *SourceMeta, r AsyncReader) map[string][]Item { 152 var p = make(map[string][]Item) 153 for _, it := range meta.Items { 154 pt := GetPatternFromItem(it, r, meta.Name) 155 if pt == "" { 156 continue 157 } 158 items := p[pt] 159 items = append(items, it) 160 p[pt] = items 161 } 162 return p 163 } 164 165 // AsyncReader helps async read files of addon 166 type AsyncReader interface { 167 // ListAddonMeta will return directory tree contain addon metadata only 168 ListAddonMeta() (addonCandidates map[string]SourceMeta, err error) 169 170 // ReadFile should accept relative path to github repo/path or OSS bucket, and report the file content 171 ReadFile(path string) (content string, err error) 172 173 // RelativePath return a relative path to GitHub repo/path or OSS bucket/path 174 RelativePath(item Item) string 175 } 176 177 // pathWithParent joins path with its parent directory, suffix slash is reserved 178 func pathWithParent(subPath, parent string) string { 179 actualPath := path.Join(parent, subPath) 180 if strings.HasSuffix(subPath, "/") { 181 actualPath += "/" 182 } 183 return actualPath 184 } 185 186 // ReaderType marks where to read addon files 187 type ReaderType string 188 189 const ( 190 gitType ReaderType = "git" 191 ossType ReaderType = "oss" 192 giteeType ReaderType = "gitee" 193 gitlabType ReaderType = "gitlab" 194 ) 195 196 // NewAsyncReader create AsyncReader from 197 // 1. GitHub url and directory 198 // 2. OSS endpoint and bucket 199 func NewAsyncReader(baseURL, bucket, repo, subPath, token string, rdType ReaderType) (AsyncReader, error) { 200 201 switch rdType { 202 case gitType: 203 baseURL = strings.TrimSuffix(baseURL, ".git") 204 u, err := url.Parse(baseURL) 205 if err != nil { 206 return nil, errors.New("addon registry invalid") 207 } 208 u.Path = path.Join(u.Path, subPath) 209 _, content, err := utils.Parse(u.String()) 210 if err != nil { 211 return nil, err 212 } 213 gith := createGitHelper(content, token) 214 return &gitReader{ 215 h: gith, 216 }, nil 217 case ossType: 218 ossURL, err := url.Parse(baseURL) 219 if err != nil { 220 return nil, err 221 } 222 var bucketEndPoint string 223 if bucket == "" { 224 bucketEndPoint = ossURL.String() 225 } else { 226 if ossURL.Scheme == "" { 227 ossURL.Scheme = "https" 228 } 229 bucketEndPoint = fmt.Sprintf(bucketTmpl, ossURL.Scheme, bucket, ossURL.Host) 230 } 231 return &ossReader{ 232 bucketEndPoint: bucketEndPoint, 233 path: subPath, 234 client: resty.New(), 235 }, nil 236 case giteeType: 237 baseURL = strings.TrimSuffix(baseURL, ".git") 238 u, err := url.Parse(baseURL) 239 if err != nil { 240 return nil, errors.New("addon registry invalid") 241 } 242 u.Path = path.Join(u.Path, subPath) 243 _, content, err := utils.Parse(u.String()) 244 if err != nil { 245 return nil, err 246 } 247 gitee := createGiteeHelper(content, token) 248 return &giteeReader{ 249 h: gitee, 250 }, nil 251 case gitlabType: 252 baseURL = strings.TrimSuffix(baseURL, ".git") 253 u, err := url.Parse(baseURL) 254 if err != nil { 255 return nil, errors.New("addon registry invalid") 256 } 257 _, content, err := utils.ParseGitlab(u.String(), repo) 258 content.GitlabContent.Path = subPath 259 if err != nil { 260 return nil, err 261 } 262 gitlabHelper, err := createGitlabHelper(content, token) 263 if err != nil { 264 return nil, errors.New("addon registry connect fail") 265 } 266 267 err = gitlabHelper.getGitlabProject(content) 268 if err != nil { 269 return nil, err 270 } 271 272 return &gitlabReader{ 273 h: gitlabHelper, 274 }, nil 275 } 276 return nil, fmt.Errorf("invalid addon registry type '%s'", rdType) 277 } 278 279 // getGitlabProject get gitlab project , set project id 280 func (h *gitlabHelper) getGitlabProject(content *utils.Content) error { 281 projectURL := content.GitlabContent.Owner + "/" + content.GitlabContent.Repo 282 projects, _, err := h.Client.Projects.GetProject(projectURL, &gitlab.GetProjectOptions{}) 283 if err != nil { 284 return err 285 } 286 content.GitlabContent.PId = projects.ID 287 288 return nil 289 } 290 291 // BuildReader will build a AsyncReader from registry, AsyncReader are needed to read addon files 292 func (r *Registry) BuildReader() (AsyncReader, error) { 293 if r.OSS != nil { 294 o := r.OSS 295 return NewAsyncReader(o.Endpoint, o.Bucket, "", o.Path, "", ossType) 296 } 297 if r.Git != nil { 298 g := r.Git 299 return NewAsyncReader(g.URL, "", "", g.Path, g.Token, gitType) 300 } 301 if r.Gitee != nil { 302 g := r.Gitee 303 return NewAsyncReader(g.URL, "", "", g.Path, g.Token, giteeType) 304 } 305 if r.Gitlab != nil { 306 g := r.Gitlab 307 return NewAsyncReader(g.URL, "", g.Repo, g.Path, g.Token, gitlabType) 308 } 309 return nil, errors.New("registry don't have enough info to build a reader") 310 } 311 312 // GetUIData get UIData of an addon 313 func (r *Registry) GetUIData(meta *SourceMeta, opt ListOptions) (*UIData, error) { 314 reader, err := r.BuildReader() 315 if err != nil { 316 return nil, err 317 } 318 addon, err := GetUIDataFromReader(reader, meta, opt) 319 if err != nil { 320 return nil, err 321 } 322 if len(addon.GlobalParameters) != 0 { 323 addon.Parameters = addon.GlobalParameters 324 } 325 addon.RegistryName = r.Name 326 return addon, nil 327 } 328 329 // ListUIData list UI data from addon registry 330 func (r *Registry) ListUIData(registryAddonMeta map[string]SourceMeta, opt ListOptions) ([]*UIData, error) { 331 reader, err := r.BuildReader() 332 if err != nil { 333 return nil, err 334 } 335 return ListAddonUIDataFromReader(reader, registryAddonMeta, r.Name, opt) 336 } 337 338 // GetInstallPackage get install package which is all needed to enable an addon from addon registry 339 func (r *Registry) GetInstallPackage(meta *SourceMeta, uiData *UIData) (*InstallPackage, error) { 340 reader, err := r.BuildReader() 341 if err != nil { 342 return nil, err 343 } 344 return GetInstallPackageFromReader(reader, meta, uiData) 345 } 346 347 // ListAddonMeta list addon file meta(path and name) from a registry 348 func (r *Registry) ListAddonMeta() (map[string]SourceMeta, error) { 349 reader, err := r.BuildReader() 350 if err != nil { 351 return nil, err 352 } 353 return reader.ListAddonMeta() 354 } 355 356 // ItemInfo contains summary information about an addon 357 type ItemInfo struct { 358 Name string 359 Description string 360 AvailableVersions []string 361 } 362 363 type itemInfoMap map[string]ItemInfo 364 365 // ListAddonInfo lists addon info (name, versions, etc.) from a registry 366 func (r *Registry) ListAddonInfo() (map[string]ItemInfo, error) { 367 addonInfoMap := make(map[string]ItemInfo) 368 369 // local registry doesn't support listing addons 370 if IsLocalRegistry(*r) { 371 return addonInfoMap, nil 372 } 373 374 if IsVersionRegistry(*r) { 375 versionedRegistry, err := ToVersionedRegistry(*r) 376 if err != nil { 377 return nil, err 378 } 379 addonList, err := versionedRegistry.ListAddon() 380 if err != nil { 381 return nil, err 382 } 383 for _, a := range addonList { 384 addonInfoMap[a.Name] = ItemInfo{ 385 Name: a.Name, 386 Description: a.Description, 387 AvailableVersions: a.AvailableVersions, 388 } 389 } 390 } else { 391 meta, err := r.ListAddonMeta() 392 if err != nil { 393 return nil, err 394 } 395 addonList, err := r.ListUIData(meta, ListOptions{}) 396 if err != nil { 397 return nil, err 398 } 399 for _, a := range addonList { 400 addonInfoMap[a.Name] = ItemInfo{ 401 Name: a.Name, 402 Description: a.Description, 403 AvailableVersions: a.AvailableVersions, 404 } 405 } 406 } 407 408 return addonInfoMap, nil 409 }