github.com/oam-dev/kubevela@v1.9.11/references/cli/registry.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 Unless required by applicable law or agreed to in writing, software 10 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 cli 18 19 import ( 20 "context" 21 "encoding/base64" 22 "encoding/xml" 23 "fmt" 24 "io" 25 "net/http" 26 "net/url" 27 "os" 28 "path" 29 "path/filepath" 30 "strings" 31 32 "github.com/google/go-github/v32/github" 33 "github.com/pkg/errors" 34 "github.com/spf13/cobra" 35 "golang.org/x/oauth2" 36 "k8s.io/apimachinery/pkg/api/meta" 37 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 38 "sigs.k8s.io/yaml" 39 40 "github.com/oam-dev/kubevela/apis/types" 41 "github.com/oam-dev/kubevela/pkg/utils/common" 42 "github.com/oam-dev/kubevela/pkg/utils/system" 43 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 44 "github.com/oam-dev/kubevela/references/docgen" 45 ) 46 47 // RegistryConfig is used to store registry config in file 48 type RegistryConfig struct { 49 Name string `json:"name"` 50 URL string `json:"url"` 51 Token string `json:"token"` 52 } 53 54 // NewRegistryCommand Manage Capability Center 55 func NewRegistryCommand(ioStream cmdutil.IOStreams, order string) *cobra.Command { 56 cmd := &cobra.Command{ 57 Use: "registry", 58 Short: "Manage Registry", 59 Long: "Manage Registry of X-Definitions for extension.", 60 Annotations: map[string]string{ 61 types.TagCommandOrder: order, 62 types.TagCommandType: types.TypeLegacy, 63 }, 64 } 65 cmd.AddCommand( 66 NewRegistryConfigCommand(ioStream), 67 NewRegistryListCommand(ioStream), 68 NewRegistryRemoveCommand(ioStream), 69 ) 70 return cmd 71 } 72 73 // NewRegistryListCommand List all registry 74 func NewRegistryListCommand(ioStreams cmdutil.IOStreams) *cobra.Command { 75 cmd := &cobra.Command{ 76 Use: "ls", 77 Aliases: []string{"list"}, 78 Short: "List all registry", 79 Long: "List all configured registry", 80 Example: `vela registry ls`, 81 RunE: func(cmd *cobra.Command, args []string) error { 82 return listCapRegistrys(ioStreams) 83 }, 84 } 85 return cmd 86 } 87 88 // NewRegistryConfigCommand Configure (add if not exist) a registry, default is local (built-in capabilities) 89 func NewRegistryConfigCommand(ioStreams cmdutil.IOStreams) *cobra.Command { 90 cmd := &cobra.Command{ 91 Use: "config <registryName> <centerURL>", 92 Short: "Configure (add if not exist) a registry, default is local (built-in capabilities)", 93 Long: "Configure (add if not exist) a registry, default is local (built-in capabilities)", 94 Example: `vela registry config my-registry https://github.com/oam-dev/catalog/tree/master/registry`, 95 RunE: func(cmd *cobra.Command, args []string) error { 96 argsLength := len(args) 97 if argsLength < 2 { 98 return errors.New("please set registry with <centerName> and <centerURL>") 99 } 100 capName := args[0] 101 capURL := args[1] 102 token := cmd.Flag("token").Value.String() 103 if err := addRegistry(capName, capURL, token); err != nil { 104 return err 105 } 106 ioStreams.Infof("Successfully configured registry %s\n", capName) 107 return nil 108 }, 109 } 110 cmd.PersistentFlags().StringP("token", "t", "", "Github Repo token") 111 return cmd 112 } 113 114 // NewRegistryRemoveCommand Remove specified registry 115 func NewRegistryRemoveCommand(ioStreams cmdutil.IOStreams) *cobra.Command { 116 cmd := &cobra.Command{ 117 Aliases: []string{"rm"}, 118 Use: "remove <centerName>", 119 Short: "Remove specified registry", 120 Long: "Remove specified registry", 121 Example: "vela registry remove mycenter", 122 RunE: func(cmd *cobra.Command, args []string) error { 123 if len(args) < 1 { 124 return errors.New("you must specify <name> for capability center you want to remove") 125 } 126 centerName := args[0] 127 msg, err := removeRegistry(centerName) 128 if err == nil { 129 ioStreams.Info(msg) 130 } 131 return err 132 }, 133 } 134 return cmd 135 } 136 137 func listCapRegistrys(ioStreams cmdutil.IOStreams) error { 138 table := newUITable() 139 table.MaxColWidth = 80 140 table.AddRow("NAME", "URL") 141 142 registrys, err := ListRegistryConfig() 143 if err != nil { 144 return errors.Wrap(err, "list registry error") 145 } 146 for _, c := range registrys { 147 tokenShow := "" 148 if len(c.Token) > 0 { 149 tokenShow = "***" 150 } 151 table.AddRow(c.Name, c.URL, tokenShow) 152 } 153 ioStreams.Info(table.String()) 154 return nil 155 } 156 157 // addRegistry will add a registry 158 func addRegistry(regName, regURL, regToken string) error { 159 regConfig := RegistryConfig{ 160 Name: regName, URL: regURL, Token: regToken, 161 } 162 repos, err := ListRegistryConfig() 163 if err != nil { 164 return err 165 } 166 var updated bool 167 for idx, r := range repos { 168 if r.Name == regConfig.Name { 169 repos[idx] = regConfig 170 updated = true 171 break 172 } 173 } 174 if !updated { 175 repos = append(repos, regConfig) 176 } 177 if err = StoreRepos(repos); err != nil { 178 return err 179 } 180 return nil 181 } 182 183 // removeRegistry will remove a registry from local 184 func removeRegistry(regName string) (string, error) { 185 var message string 186 var err error 187 188 regConfigs, err := ListRegistryConfig() 189 if err != nil { 190 return message, err 191 } 192 found := false 193 for idx, r := range regConfigs { 194 if r.Name == regName { 195 regConfigs = append(regConfigs[:idx], regConfigs[idx+1:]...) 196 found = true 197 break 198 } 199 } 200 if !found { 201 return fmt.Sprintf("registry %s not found", regName), nil 202 } 203 if err = StoreRepos(regConfigs); err != nil { 204 return message, err 205 } 206 message = fmt.Sprintf("Successfully remove registry %s", regName) 207 return message, err 208 } 209 210 // DefaultRegistry is default registry 211 const DefaultRegistry = "default" 212 213 // Registry define a registry used to get and list types.Capability 214 type Registry interface { 215 GetName() string 216 GetURL() string 217 GetCap(addonName string) (types.Capability, []byte, error) 218 ListCaps() ([]types.Capability, error) 219 } 220 221 // GithubRegistry is Registry's implementation treat github url as resource 222 type GithubRegistry struct { 223 URL string `json:"url"` 224 RegistryName string `json:"registry_name"` 225 client *github.Client 226 cfg *GithubContent 227 ctx context.Context 228 } 229 230 // NewRegistryFromConfig return Registry interface to get capabilities 231 func NewRegistryFromConfig(config RegistryConfig) (Registry, error) { 232 return NewRegistry(context.TODO(), config.Token, config.Name, config.URL) 233 } 234 235 // NewRegistry will create a registry implementation 236 func NewRegistry(ctx context.Context, token, registryName string, regURL string) (Registry, error) { 237 tp, cfg, err := Parse(regURL) 238 if err != nil { 239 return nil, err 240 } 241 switch tp { 242 case TypeGithub: 243 var tc *http.Client 244 if token != "" { 245 ts := oauth2.StaticTokenSource( 246 &oauth2.Token{AccessToken: token}, 247 ) 248 tc = oauth2.NewClient(ctx, ts) 249 } 250 return GithubRegistry{ 251 URL: cfg.URL, 252 RegistryName: registryName, 253 client: github.NewClient(tc), 254 cfg: &cfg.GithubContent, 255 ctx: ctx, 256 }, nil 257 case TypeOss: 258 var tc http.Client 259 return OssRegistry{ 260 Client: &tc, 261 BucketURL: fmt.Sprintf("https://%s/", cfg.BucketURL), 262 RegistryName: registryName, 263 }, nil 264 case TypeLocal: 265 _, err := os.Stat(cfg.AbsDir) 266 if os.IsNotExist(err) { 267 return LocalRegistry{}, err 268 } 269 return LocalRegistry{ 270 AbsPath: cfg.AbsDir, 271 RegistryName: registryName, 272 }, nil 273 case TypeUnknown: 274 return nil, fmt.Errorf("not supported url") 275 } 276 277 return nil, fmt.Errorf("not supported url") 278 } 279 280 // ListRegistryConfig will get all registry config stored in local 281 // this will return at least one config, which is DefaultRegistry 282 func ListRegistryConfig() ([]RegistryConfig, error) { 283 defaultRegistryConfig := RegistryConfig{Name: DefaultRegistry, URL: "oss://registry.kubevela.net/"} 284 config, err := system.GetRepoConfig() 285 if err != nil { 286 return nil, err 287 } 288 data, err := os.ReadFile(filepath.Clean(config)) 289 if err != nil { 290 if os.IsNotExist(err) { 291 err := StoreRepos([]RegistryConfig{defaultRegistryConfig}) 292 if err != nil { 293 return nil, errors.Wrap(err, "error initialize default registry") 294 } 295 return ListRegistryConfig() 296 } 297 return nil, err 298 } 299 var regConfigs []RegistryConfig 300 if err = yaml.Unmarshal(data, ®Configs); err != nil { 301 return nil, err 302 } 303 haveDefault := false 304 for _, r := range regConfigs { 305 if r.URL == defaultRegistryConfig.URL { 306 haveDefault = true 307 break 308 } 309 } 310 if !haveDefault { 311 regConfigs = append(regConfigs, defaultRegistryConfig) 312 } 313 return regConfigs, nil 314 } 315 316 // GetRegistry get a Registry implementation by name 317 func GetRegistry(regName string) (Registry, error) { 318 regConfigs, err := ListRegistryConfig() 319 if err != nil { 320 return nil, err 321 } 322 for _, conf := range regConfigs { 323 if conf.Name == regName { 324 return NewRegistryFromConfig(conf) 325 } 326 } 327 return nil, errors.Errorf("registry %s not found", regName) 328 } 329 330 // GetName will return registry name 331 func (g GithubRegistry) GetName() string { 332 return g.RegistryName 333 } 334 335 // GetURL will return github registry url 336 func (g GithubRegistry) GetURL() string { 337 return g.cfg.URL 338 } 339 340 // ListCaps list all capabilities of registry 341 func (g GithubRegistry) ListCaps() ([]types.Capability, error) { 342 var addons []types.Capability 343 344 itemContents, err := g.getRepoFile() 345 if err != nil { 346 return []types.Capability{}, err 347 } 348 for _, item := range itemContents { 349 capa, err := item.toCapability() 350 if err != nil { 351 fmt.Printf("parse definition of %s err %v\n", item.name, err) 352 continue 353 } 354 addons = append(addons, capa) 355 } 356 return addons, nil 357 } 358 359 // GetCap return capability object and raw data specified by cap name 360 func (g GithubRegistry) GetCap(addonName string) (types.Capability, []byte, error) { 361 fileContent, _, _, err := g.client.Repositories.GetContents(context.Background(), g.cfg.Owner, g.cfg.Repo, fmt.Sprintf("%s/%s.yaml", g.cfg.Path, addonName), &github.RepositoryContentGetOptions{Ref: g.cfg.Ref}) 362 if err != nil { 363 return types.Capability{}, []byte{}, err 364 } 365 var data []byte 366 if *fileContent.Encoding == "base64" { 367 data, err = base64.StdEncoding.DecodeString(*fileContent.Content) 368 if err != nil { 369 fmt.Printf("decode github content %s err %s\n", fileContent.GetPath(), err) 370 } 371 } 372 repoFile := RegistryFile{ 373 data: data, 374 name: *fileContent.Name, 375 } 376 capa, err := repoFile.toCapability() 377 if err != nil { 378 return types.Capability{}, []byte{}, err 379 } 380 capa.Source = &types.Source{RepoName: g.RegistryName} 381 return capa, data, nil 382 } 383 384 func (g *GithubRegistry) getRepoFile() ([]RegistryFile, error) { 385 var items []RegistryFile 386 _, dirs, _, err := g.client.Repositories.GetContents(g.ctx, g.cfg.Owner, g.cfg.Repo, g.cfg.Path, &github.RepositoryContentGetOptions{Ref: g.cfg.Ref}) 387 if err != nil { 388 return []RegistryFile{}, err 389 } 390 for _, repoItem := range dirs { 391 if *repoItem.Type != "file" { 392 continue 393 } 394 fileContent, _, _, err := g.client.Repositories.GetContents(g.ctx, g.cfg.Owner, g.cfg.Repo, *repoItem.Path, &github.RepositoryContentGetOptions{Ref: g.cfg.Ref}) 395 if err != nil { 396 fmt.Printf("Getting content URL %s error: %s\n", repoItem.GetURL(), err) 397 continue 398 } 399 var data []byte 400 if *fileContent.Encoding == "base64" { 401 data, err = base64.StdEncoding.DecodeString(*fileContent.Content) 402 if err != nil { 403 fmt.Printf("decode github content %s err %s\n", fileContent.GetPath(), err) 404 continue 405 } 406 } 407 items = append(items, RegistryFile{ 408 data: data, 409 name: *fileContent.Name, 410 }) 411 } 412 return items, nil 413 } 414 415 // OssRegistry is Registry's implementation treat OSS url as resource 416 type OssRegistry struct { 417 *http.Client `json:"-"` 418 BucketURL string `json:"bucket_url"` 419 RegistryName string `json:"registry_name"` 420 } 421 422 // GetName return name of OssRegistry 423 func (o OssRegistry) GetName() string { 424 return o.RegistryName 425 } 426 427 // GetURL return URL of OssRegistry's bucket 428 func (o OssRegistry) GetURL() string { 429 return o.BucketURL 430 } 431 432 // GetCap return capability object and raw data specified by cap name 433 func (o OssRegistry) GetCap(addonName string) (types.Capability, []byte, error) { 434 filename := addonName + ".yaml" 435 req, _ := http.NewRequestWithContext( 436 context.Background(), 437 http.MethodGet, 438 o.BucketURL+filename, 439 nil, 440 ) 441 resp, err := o.Client.Do(req) 442 if err != nil { 443 return types.Capability{}, nil, err 444 } 445 data, err := io.ReadAll(resp.Body) 446 _ = resp.Body.Close() 447 if err != nil { 448 return types.Capability{}, nil, err 449 } 450 rf := RegistryFile{ 451 data: data, 452 name: filename, 453 } 454 capa, err := rf.toCapability() 455 if err != nil { 456 return types.Capability{}, nil, err 457 } 458 capa.Source = &types.Source{RepoName: o.RegistryName} 459 460 return capa, data, nil 461 } 462 463 // ListCaps list all capabilities of registry 464 func (o OssRegistry) ListCaps() ([]types.Capability, error) { 465 rfs, err := o.getRegFiles() 466 if err != nil { 467 return []types.Capability{}, errors.Wrap(err, "Get raw files fail") 468 } 469 capas := make([]types.Capability, 0) 470 471 for _, rf := range rfs { 472 capa, err := rf.toCapability() 473 if err != nil { 474 fmt.Printf("[WARN] Parse file %s fail: %s\n", rf.name, err.Error()) 475 } 476 capas = append(capas, capa) 477 } 478 return capas, nil 479 } 480 481 func (o OssRegistry) getRegFiles() ([]RegistryFile, error) { 482 req, _ := http.NewRequestWithContext( 483 context.Background(), 484 http.MethodGet, 485 o.BucketURL+"?list-type=2", 486 nil, 487 ) 488 resp, err := o.Client.Do(req) 489 if err != nil { 490 return []RegistryFile{}, err 491 } 492 data, err := io.ReadAll(resp.Body) 493 _ = resp.Body.Close() 494 if err != nil { 495 return []RegistryFile{}, err 496 } 497 list := &ListBucketResult{} 498 err = xml.Unmarshal(data, list) 499 if err != nil { 500 return []RegistryFile{}, err 501 } 502 rfs := make([]RegistryFile, 0) 503 504 for _, fileName := range list.File { 505 req, _ := http.NewRequestWithContext( 506 context.Background(), 507 http.MethodGet, 508 o.BucketURL+fileName, 509 nil, 510 ) 511 resp, err := o.Client.Do(req) 512 if err != nil { 513 fmt.Printf("[WARN] %s download fail\n", fileName) 514 continue 515 } 516 data, _ := io.ReadAll(resp.Body) 517 _ = resp.Body.Close() 518 rf := RegistryFile{ 519 data: data, 520 name: fileName, 521 } 522 rfs = append(rfs, rf) 523 524 } 525 return rfs, nil 526 } 527 528 // LocalRegistry is Registry's implementation treat local url as resource 529 type LocalRegistry struct { 530 AbsPath string `json:"abs_path"` 531 RegistryName string `json:"registry_name"` 532 } 533 534 // GetName return name of LocalRegistry 535 func (l LocalRegistry) GetName() string { 536 return l.RegistryName 537 } 538 539 // GetURL return path of LocalRegistry 540 func (l LocalRegistry) GetURL() string { 541 return l.AbsPath 542 } 543 544 // GetCap return capability object and raw data specified by cap name 545 func (l LocalRegistry) GetCap(addonName string) (types.Capability, []byte, error) { 546 fileName := addonName + ".yaml" 547 filePath := fmt.Sprintf("%s/%s", l.AbsPath, fileName) 548 data, err := os.ReadFile(filePath) // nolint 549 if err != nil { 550 return types.Capability{}, []byte{}, err 551 } 552 file := RegistryFile{ 553 data: data, 554 name: fileName, 555 } 556 capa, err := file.toCapability() 557 if err != nil { 558 return types.Capability{}, []byte{}, err 559 } 560 capa.Source = &types.Source{RepoName: l.RegistryName} 561 562 return capa, data, nil 563 } 564 565 // ListCaps list all capabilities of registry 566 func (l LocalRegistry) ListCaps() ([]types.Capability, error) { 567 glob := filepath.Join(filepath.Clean(l.AbsPath), "*") 568 files, _ := filepath.Glob(glob) 569 capas := make([]types.Capability, 0) 570 for _, file := range files { 571 // nolint:gosec 572 data, err := os.ReadFile(file) 573 if err != nil { 574 return nil, err 575 } 576 capa, err := RegistryFile{ 577 data: data, 578 name: path.Base(file), 579 }.toCapability() 580 if err != nil { 581 fmt.Printf("parsing file: %s err: %s\n", file, err) 582 continue 583 } 584 capas = append(capas, capa) 585 } 586 return capas, nil 587 } 588 589 func (item RegistryFile) toCapability() (types.Capability, error) { 590 cli, err := (&common.Args{}).GetClient() 591 if err != nil { 592 return types.Capability{}, err 593 } 594 capability, err := ParseCapability(cli.RESTMapper(), item.data) 595 if err != nil { 596 return types.Capability{}, err 597 } 598 return capability, nil 599 } 600 601 // RegistryFile describes a file item in registry 602 type RegistryFile struct { 603 data []byte // file content 604 name string // file's name 605 } 606 607 // ListBucketResult describe a file list from OSS 608 type ListBucketResult struct { 609 File []string `xml:"Contents>Key"` 610 Count int `xml:"KeyCount"` 611 } 612 613 // Content contains different type of content needed when building Registry 614 type Content struct { 615 OssContent 616 GithubContent 617 LocalContent 618 } 619 620 // LocalContent for local registry 621 type LocalContent struct { 622 AbsDir string `json:"abs_dir"` 623 } 624 625 // OssContent for oss registry 626 type OssContent struct { 627 BucketURL string `json:"bucket_url"` 628 } 629 630 // GithubContent for registry 631 type GithubContent struct { 632 URL string `json:"url"` 633 Owner string `json:"owner"` 634 Repo string `json:"repo"` 635 Path string `json:"path"` 636 Ref string `json:"ref"` 637 } 638 639 // TypeLocal represents github 640 const TypeLocal = "local" 641 642 // TypeOss represent oss 643 const TypeOss = "oss" 644 645 // TypeGithub represents github 646 const TypeGithub = "github" 647 648 // TypeUnknown represents parse failed 649 const TypeUnknown = "unknown" 650 651 // Parse will parse config from address 652 func Parse(addr string) (string, *Content, error) { 653 URL, err := url.Parse(addr) 654 if err != nil { 655 return "", nil, err 656 } 657 l := strings.Split(strings.TrimPrefix(URL.Path, "/"), "/") 658 switch URL.Scheme { 659 case "http", "https": 660 switch URL.Host { 661 case "github.com": 662 // We support two valid format: 663 // 1. https://github.com/<owner>/<repo>/tree/<branch>/<path-to-dir> 664 // 2. https://github.com/<owner>/<repo>/<path-to-dir> 665 if len(l) < 3 { 666 return "", nil, errors.New("invalid format " + addr) 667 } 668 if l[2] == "tree" { 669 // https://github.com/<owner>/<repo>/tree/<branch>/<path-to-dir> 670 if len(l) < 5 { 671 return "", nil, errors.New("invalid format " + addr) 672 } 673 return TypeGithub, &Content{ 674 GithubContent: GithubContent{ 675 URL: addr, 676 Owner: l[0], 677 Repo: l[1], 678 Path: strings.Join(l[4:], "/"), 679 Ref: l[3], 680 }, 681 }, nil 682 } 683 // https://github.com/<owner>/<repo>/<path-to-dir> 684 return TypeGithub, &Content{ 685 GithubContent: GithubContent{ 686 URL: addr, 687 Owner: l[0], 688 Repo: l[1], 689 Path: strings.Join(l[2:], "/"), 690 Ref: "", // use default branch 691 }, 692 }, 693 nil 694 case "api.github.com": 695 if len(l) != 5 { 696 return "", nil, errors.New("invalid format " + addr) 697 } 698 //https://api.github.com/repos/<owner>/<repo>/contents/<path-to-dir> 699 return TypeGithub, &Content{ 700 GithubContent: GithubContent{ 701 URL: addr, 702 Owner: l[1], 703 Repo: l[2], 704 Path: l[4], 705 Ref: URL.Query().Get("ref"), 706 }, 707 }, 708 nil 709 default: 710 } 711 case "oss": 712 return TypeOss, &Content{ 713 OssContent: OssContent{ 714 BucketURL: URL.Host, 715 }, 716 }, nil 717 case "file": 718 return TypeLocal, &Content{ 719 LocalContent: LocalContent{ 720 AbsDir: URL.Path, 721 }, 722 }, nil 723 724 } 725 726 return TypeUnknown, nil, nil 727 } 728 729 // StoreRepos will store registry repo locally 730 func StoreRepos(registries []RegistryConfig) error { 731 config, err := system.GetRepoConfig() 732 if err != nil { 733 return err 734 } 735 data, err := yaml.Marshal(registries) 736 if err != nil { 737 return err 738 } 739 //nolint:gosec 740 return os.WriteFile(config, data, 0644) 741 } 742 743 // ParseCapability will convert config from remote center to capability 744 func ParseCapability(mapper meta.RESTMapper, data []byte) (types.Capability, error) { 745 var obj = unstructured.Unstructured{Object: make(map[string]interface{})} 746 err := yaml.Unmarshal(data, &obj.Object) 747 if err != nil { 748 return types.Capability{}, err 749 } 750 return docgen.ParseCapabilityFromUnstructured(mapper, nil, obj) 751 }