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, &regConfigs); 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  }