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 }