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  }