github.com/oam-dev/kubevela@v1.9.11/pkg/addon/versioned_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  
    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  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"sort"
    24  
    25  	"github.com/Masterminds/semver/v3"
    26  	"github.com/pkg/errors"
    27  	"helm.sh/helm/v3/pkg/chart/loader"
    28  	"helm.sh/helm/v3/pkg/repo"
    29  	"k8s.io/klog/v2"
    30  
    31  	"github.com/oam-dev/kubevela/pkg/utils"
    32  	"github.com/oam-dev/kubevela/pkg/utils/common"
    33  	"github.com/oam-dev/kubevela/pkg/utils/helm"
    34  )
    35  
    36  const (
    37  	// velaSystemRequirement is the vela version requirement annotation key
    38  	velaSystemRequirement = `system.vela`
    39  	// kubernetesSystemRequirement is the kubernetes requirement annotation key
    40  	kubernetesSystemRequirement = `system.kubernetes`
    41  	// addonSystemRequirement is the annotation key to identity an addon from helm chart structure
    42  	addonSystemRequirement = `addon.name`
    43  )
    44  
    45  // VersionedRegistry is the interface of support version registry
    46  type VersionedRegistry interface {
    47  	ListAddon() ([]*UIData, error)
    48  	GetAddonUIData(ctx context.Context, addonName, version string) (*UIData, error)
    49  	GetAddonInstallPackage(ctx context.Context, addonName, version string) (*InstallPackage, error)
    50  	GetDetailedAddon(ctx context.Context, addonName, version string) (*WholeAddonPackage, error)
    51  	GetAddonAvailableVersion(addonName string) ([]*repo.ChartVersion, error)
    52  }
    53  
    54  // BuildVersionedRegistry is build versioned addon registry
    55  func BuildVersionedRegistry(name, repoURL string, opts *common.HTTPOption) VersionedRegistry {
    56  	return &versionedRegistry{
    57  		name: name,
    58  		url:  repoURL,
    59  		h:    helm.NewHelperWithCache(),
    60  		Opts: opts,
    61  	}
    62  }
    63  
    64  // ToVersionedRegistry converts registry to versioned registry
    65  func ToVersionedRegistry(registry Registry) (VersionedRegistry, error) {
    66  	if !IsVersionRegistry(registry) {
    67  		return nil, errors.Errorf("registry '%s' is not a versioned registry", registry.Name)
    68  	}
    69  	return BuildVersionedRegistry(registry.Name, registry.Helm.URL, &common.HTTPOption{
    70  		Username:        registry.Helm.Username,
    71  		Password:        registry.Helm.Password,
    72  		InsecureSkipTLS: registry.Helm.InsecureSkipTLS,
    73  	}), nil
    74  }
    75  
    76  type versionedRegistry struct {
    77  	url  string
    78  	name string
    79  	h    *helm.Helper
    80  	// username and password for registry needs basic auth
    81  	Opts *common.HTTPOption
    82  }
    83  
    84  func (i *versionedRegistry) ListAddon() ([]*UIData, error) {
    85  	chartIndex, err := i.h.GetIndexInfo(i.url, false, i.Opts)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	return i.resolveAddonListFromIndex(i.name, chartIndex), nil
    90  }
    91  
    92  func (i *versionedRegistry) GetAddonUIData(ctx context.Context, addonName, version string) (*UIData, error) {
    93  	wholePackage, err := i.loadAddon(ctx, addonName, version)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	return &UIData{
    98  		Meta:              wholePackage.Meta,
    99  		APISchema:         wholePackage.APISchema,
   100  		Parameters:        wholePackage.Parameters,
   101  		Detail:            wholePackage.Detail,
   102  		Definitions:       wholePackage.Definitions,
   103  		AvailableVersions: wholePackage.AvailableVersions,
   104  		CUEDefinitions:    wholePackage.CUEDefinitions,
   105  	}, nil
   106  }
   107  
   108  func (i *versionedRegistry) GetAddonInstallPackage(ctx context.Context, addonName, version string) (*InstallPackage, error) {
   109  	wholePackage, err := i.loadAddon(ctx, addonName, version)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return &wholePackage.InstallPackage, nil
   114  }
   115  
   116  func (i *versionedRegistry) GetDetailedAddon(ctx context.Context, addonName, version string) (*WholeAddonPackage, error) {
   117  	wholePackage, err := i.loadAddon(ctx, addonName, version)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	return wholePackage, nil
   122  }
   123  
   124  // GetAddonAvailableVersion will return all available versions of the addon which is loaded from the registry, and the version are sorted from last to first
   125  func (i versionedRegistry) GetAddonAvailableVersion(addonName string) ([]*repo.ChartVersion, error) {
   126  	return i.loadAddonVersions(addonName)
   127  }
   128  
   129  func (i *versionedRegistry) resolveAddonListFromIndex(repoName string, index *repo.IndexFile) []*UIData {
   130  	var res []*UIData
   131  	for addonName, versions := range index.Entries {
   132  		if len(versions) == 0 {
   133  			continue
   134  		}
   135  		sort.Sort(sort.Reverse(versions))
   136  		latestVersion := versions[0]
   137  		var availableVersions []string
   138  		for _, version := range versions {
   139  			availableVersions = append(availableVersions, version.Version)
   140  		}
   141  		o := UIData{Meta: Meta{
   142  			Name:        addonName,
   143  			Icon:        latestVersion.Icon,
   144  			Tags:        latestVersion.Keywords,
   145  			Description: latestVersion.Description,
   146  			Version:     latestVersion.Version,
   147  		}, RegistryName: repoName, AvailableVersions: availableVersions}
   148  		res = append(res, &o)
   149  	}
   150  	return res
   151  }
   152  
   153  func (i versionedRegistry) loadAddon(ctx context.Context, name, version string) (*WholeAddonPackage, error) {
   154  	versions, err := i.h.ListVersions(i.url, name, false, i.Opts)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	if len(versions) == 0 {
   159  		return nil, ErrNotExist
   160  	}
   161  	sort.Sort(sort.Reverse(versions))
   162  	addonVersion, availableVersions := chooseVersion(version, versions)
   163  	if addonVersion == nil {
   164  		return nil, errors.Errorf("specified version %s for addon %s not exist", utils.Sanitize(version), name)
   165  	}
   166  	for _, chartURL := range addonVersion.URLs {
   167  		if !utils.IsValidURL(chartURL) {
   168  			chartURL, err = utils.JoinURL(i.url, chartURL)
   169  			if err != nil {
   170  				return nil, fmt.Errorf("cannot join versionedRegistryURL %s and chartURL %s, %w", i.url, chartURL, err)
   171  			}
   172  		}
   173  		archive, err := common.HTTPGetWithOption(ctx, chartURL, i.Opts)
   174  		if err != nil {
   175  			klog.Warningf("failed to download the addon package %s:%s", chartURL, err.Error())
   176  			continue
   177  		}
   178  		bufferedFile, err := loader.LoadArchiveFiles(bytes.NewReader(archive))
   179  		if err != nil {
   180  			klog.Warningf("failed to load the addon package:%s", err.Error())
   181  			continue
   182  		}
   183  		addonPkg, err := loadAddonPackage(name, bufferedFile)
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  		addonPkg.AvailableVersions = availableVersions
   188  		addonPkg.RegistryName = i.name
   189  		addonPkg.Meta.SystemRequirements = LoadSystemRequirements(addonVersion.Annotations)
   190  		if addonPkg.Name != "" {
   191  			klog.V(5).Infof("Addon '%s' with version '%s' loaded successfully from registry '%s'", addonVersion.Name, addonVersion.Version, i.name)
   192  		}
   193  		return addonPkg, nil
   194  	}
   195  	return nil, ErrFetch
   196  }
   197  
   198  // loadAddonVersions Load all available versions of the addon
   199  func (i versionedRegistry) loadAddonVersions(addonName string) ([]*repo.ChartVersion, error) {
   200  	versions, err := i.h.ListVersions(i.url, addonName, false, i.Opts)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	if len(versions) == 0 {
   205  		return nil, ErrNotExist
   206  	}
   207  	sort.Sort(sort.Reverse(versions))
   208  	return versions, nil
   209  }
   210  
   211  func loadAddonPackage(addonName string, files []*loader.BufferedFile) (*WholeAddonPackage, error) {
   212  	mr := MemoryReader{Name: addonName, Files: files}
   213  	metas, err := mr.ListAddonMeta()
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	meta := metas[addonName]
   218  	addonUIData, err := GetUIDataFromReader(&mr, &meta, UIMetaOptions)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	installPackage, err := GetInstallPackageFromReader(&mr, &meta, addonUIData)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return &WholeAddonPackage{
   227  		InstallPackage: *installPackage,
   228  		Detail:         addonUIData.Detail,
   229  		APISchema:      addonUIData.APISchema,
   230  	}, nil
   231  }
   232  
   233  // chooseVersion will return the target version and all available versions
   234  // This function is not sensitive to v-prefix, which means if specifiedVersion=0.3.0, v0.3.0 can be chosen.
   235  func chooseVersion(specifiedVersion string, versions []*repo.ChartVersion) (*repo.ChartVersion, []string) {
   236  	var addonVersion *repo.ChartVersion
   237  	var availableVersions []string
   238  	for i, v := range versions {
   239  		availableVersions = append(availableVersions, v.Version)
   240  		if addonVersion != nil {
   241  			// already find the latest not-prerelease version, skip the find
   242  			continue
   243  		}
   244  		if len(specifiedVersion) != 0 {
   245  			if utils.IgnoreVPrefix(v.Version) == utils.IgnoreVPrefix(specifiedVersion) {
   246  				addonVersion = versions[i]
   247  			}
   248  		} else {
   249  			vv, err := semver.NewVersion(v.Version)
   250  			if err != nil {
   251  				continue
   252  			}
   253  			if len(vv.Prerelease()) != 0 {
   254  				continue
   255  			}
   256  			addonVersion = v
   257  		}
   258  	}
   259  	return addonVersion, availableVersions
   260  }
   261  
   262  // LoadSystemRequirements load the system version requirements from the addon's meta file
   263  func LoadSystemRequirements(anno map[string]string) *SystemRequirements {
   264  	if len(anno) == 0 {
   265  		return nil
   266  	}
   267  	req := &SystemRequirements{}
   268  	if _, ok := anno[velaSystemRequirement]; ok {
   269  		req.VelaVersion = anno[velaSystemRequirement]
   270  	}
   271  	if _, ok := anno[kubernetesSystemRequirement]; ok {
   272  		req.KubernetesVersion = anno[kubernetesSystemRequirement]
   273  	}
   274  	return req
   275  }