From cdafc03e7d513cd787ba69029bba17dc0bed1e0b Mon Sep 17 00:00:00 2001 From: Siege Lion <1448189829@qq.com> Date: Wed, 29 Jun 2022 17:46:56 +0800 Subject: [PATCH] Feat: search useful addon version automatically (#4232) * Feat: search useful addon version automatically Verify whether the current addon version meets the system version requirements according to the obtained specified version. There are two system version requirements: Vela core version, K8s version. If meet the requirements and continue to perform the next task. If the requirements are not met, obtain the highest version that meets the requirements Refs #4181 Signed-off-by: HanMengnan <1448189829@qq.com> * Fix: Optimize function implementation and code order, and modify test cases add more comments of function optimize package import sequence optimize user interaction logic and error information extraction logic Signed-off-by: HanMengnan <1448189829@qq.com> * Fix: change template string of regular expression to const type string Signed-off-by: HanMengnan <1448189829@qq.com> --- e2e/addon/addon_test.go | 28 ++++- .../helm-repo/fluxcd-test-version-1.0.0.tgz | Bin 0 -> 376 bytes .../helm-repo/fluxcd-test-version-2.0.0.tgz | Bin 0 -> 377 bytes e2e/addon/mock/testrepo/helm-repo/index.yaml | 24 +++++ e2e/addon/mock/utils/utils.go | 3 +- e2e/addon/mock/vela_addon_mock_server.go | 25 +++++ pkg/addon/addon.go | 24 ++++- pkg/addon/addon_test.go | 40 +++++++ pkg/addon/error.go | 18 +++- pkg/addon/error_test.go | 43 ++++++++ .../multiversion-helm-repo/fluxcd-1.0.0.tgz | Bin 0 -> 26631 bytes .../multiversion-helm-repo/fluxcd-2.0.0.tgz | Bin 0 -> 26298 bytes .../multiversion-helm-repo/index.yaml | 34 ++++++ pkg/addon/versioned_registry.go | 48 ++++++++- pkg/addon/versioned_registry_test.go | 102 +++++++++++++++++- references/cli/addon.go | 31 +++--- test/e2e-apiserver-test/addon_test.go | 8 +- 17 files changed, 402 insertions(+), 26 deletions(-) create mode 100644 e2e/addon/mock/testrepo/helm-repo/fluxcd-test-version-1.0.0.tgz create mode 100644 e2e/addon/mock/testrepo/helm-repo/fluxcd-test-version-2.0.0.tgz create mode 100644 e2e/addon/mock/testrepo/helm-repo/index.yaml create mode 100644 pkg/addon/error_test.go create mode 100644 pkg/addon/testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz create mode 100644 pkg/addon/testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz create mode 100644 pkg/addon/testdata/multiversion-helm-repo/index.yaml diff --git a/e2e/addon/addon_test.go b/e2e/addon/addon_test.go index 17144fc5a..d1497950f 100644 --- a/e2e/addon/addon_test.go +++ b/e2e/addon/addon_test.go @@ -22,10 +22,10 @@ import ( "strings" "time" - v1 "k8s.io/api/core/v1" - + "github.com/Netflix/go-expect" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -110,6 +110,30 @@ var _ = Describe("Addon Test", func() { g.Expect(apierrors.IsNotFound(k8sClient.Get(context.Background(), types.NamespacedName{Name: "addon-test-addon", Namespace: "test-vela"}, &v1beta1.Application{}))).Should(BeTrue()) }, 60*time.Second).Should(Succeed()) }) + + It("Enable fluxcd-test-version whose version can't suit system requirements", func() { + output, err := e2e.InteractiveExec("vela addon enable fluxcd-test-version", func(c *expect.Console) { + _, err = c.SendLine("y") + Expect(err).NotTo(HaveOccurred()) + }) + Expect(output).To(ContainSubstring("enabled successfully")) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Disable addon fluxcd-test-version", func() { + output, err := e2e.LongTimeExec("vela addon disable fluxcd-test-version", 600*time.Second) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("Successfully disable addon")) + }) + + It("Enable fluxcd-test-version whose version can't suit system requirements with 'n' input", func() { + output, err := e2e.InteractiveExec("vela addon enable fluxcd-test-version", func(c *expect.Console) { + _, err = c.SendLine("n") + Expect(err).NotTo(HaveOccurred()) + }) + Expect(output).To(ContainSubstring("you can try another version by command")) + Expect(err).NotTo(HaveOccurred()) + }) }) Context("Addon registry test", func() { diff --git a/e2e/addon/mock/testrepo/helm-repo/fluxcd-test-version-1.0.0.tgz b/e2e/addon/mock/testrepo/helm-repo/fluxcd-test-version-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..132ead7c76793d84c56d3efd055320f73de2418e GIT binary patch literal 376 zcmb2|=3oE==C_y5<{ehxVR$gFk}Gb3O6KgaQ?>aUs$H9JFA|h#e0lzMf684o-@8>2 zOEXvP?^bp`kPx!5^yQUVnvP3eGCFN}(S0&@>djT_j?bv=eO$hE#?`MmX6wT$<4oiA z#DpX7Z|`{>q$0ay4vWjL%zJA;)_cXit!H1-tG4#suHyaok~2PBxbXg0eE8}9f74f2 zzWZ-4NTMTmgKh81<-(2T2~PwMcRS~*EHu%G z{*qFANMz|Zw{MK)j7eMAC$PovJ=DpuUb8{{RdTS1qx{u#m+W$bS`7a1&kO0k`!u$1 z;foR#j@E-_Vz1Yh3Un*KVD@Pby!v~t$H7{?U*A@5(qMTRn{y=O(9%a=tB+bv|9;f2 zt7gl+hd(8y6^{$El)S!|IbXlcsc8bBt=!?l+mD#GU$C!PM<9D@~)%9#PuU2_p zHD4|k6ItK4jO$V)9i*HDMEYsot?_?KUG0Adq>U!6&bo8I-eYL+(9rMtUsubI{?8XV zUoGcl?tgiQ^WF5|{r9?qMqYUX;u0G7|}f!GGy)0lcX5qJYe9x=Q))J76YgR(_W z)+bC+YHws8a2Fs(djM&`=1.3.0; kubernetes>=1.10.0" + - apiVersion: v2 + appVersion: 1.16.0 + description: A Helm chart for Kubernetes + name: fluxcd-test-version + type: application + urls: + - http://127.0.0.1:9098/helm/fluxcd-test-version-2.0.0.tgz + version: 2.0.0 + annotations: + system: "vela>=1.5.0; kubernetes>=1.30.0" +generated: "2022-06-15T13:17:04.733573+08:00" \ No newline at end of file diff --git a/e2e/addon/mock/utils/utils.go b/e2e/addon/mock/utils/utils.go index 93a9f6c92..63fe363bd 100644 --- a/e2e/addon/mock/utils/utils.go +++ b/e2e/addon/mock/utils/utils.go @@ -39,7 +39,7 @@ var ( apiVersion: v1 data: registries: '{ "KubeVela":{ "name": "KubeVela", "oss": { "end_point": "http://REGISTRY_ADDR", - "bucket": "" } } }' + "bucket": "" } }, "Test-Helm":{ "name": "Test-Helm", "helm": { "name":"", "password":"", "url": "http://HELM_ADDR"} } }' kind: ConfigMap metadata: name: vela-addon-registry @@ -59,6 +59,7 @@ func ApplyMockServerConfig() error { cm := v1.ConfigMap{} registryCmStr := strings.ReplaceAll(velaRegistry, "REGISTRY_ADDR", fmt.Sprintf("127.0.0.1:%d", Port)) + registryCmStr = strings.ReplaceAll(registryCmStr, "HELM_ADDR", fmt.Sprintf("127.0.0.1:%d/helm", Port)) err = yaml.Unmarshal([]byte(registryCmStr), &cm) if err != nil { diff --git a/e2e/addon/mock/vela_addon_mock_server.go b/e2e/addon/mock/vela_addon_mock_server.go index 96a350708..b06f085c1 100644 --- a/e2e/addon/mock/vela_addon_mock_server.go +++ b/e2e/addon/mock/vela_addon_mock_server.go @@ -22,6 +22,7 @@ import ( "fmt" "html/template" "io/fs" + "io/ioutil" "log" "net/http" "path" @@ -46,6 +47,7 @@ func main() { log.Fatal("Apply mock server config to ConfigMap fail") } http.HandleFunc("/", ossHandler) + http.HandleFunc("/helm/", helmHandler) err = http.ListenAndServe(fmt.Sprintf(":%d", utils.Port), nil) if err != nil { log.Fatal("ListenAndServe: ", err) @@ -103,6 +105,29 @@ var ossHandler http.HandlerFunc = func(rw http.ResponseWriter, req *http.Request } } +var helmHandler http.HandlerFunc = func(rw http.ResponseWriter, req *http.Request) { + switch { + case strings.Contains(req.URL.Path, "index.yaml"): + file, err := ioutil.ReadFile("./e2e/addon/mock/testrepo/helm-repo/index.yaml") + if err != nil { + _, _ = rw.Write([]byte(err.Error())) + } + rw.Write(file) + case strings.Contains(req.URL.Path, "fluxcd-test-version-1.0.0.tgz"): + file, err := ioutil.ReadFile("./e2e/addon/mock/testrepo/helm-repo/fluxcd-test-version-1.0.0.tgz") + if err != nil { + _, _ = rw.Write([]byte(err.Error())) + } + rw.Write(file) + case strings.Contains(req.URL.Path, "fluxcd-test-version-2.0.0.tgz"): + file, err := ioutil.ReadFile("./e2e/addon/mock/testrepo/helm-repo/fluxcd-test-version-2.0.0.tgz") + if err != nil { + _, _ = rw.Write([]byte(err.Error())) + } + rw.Write(file) + } +} + func init() { _ = fs.WalkDir(testData, "testdata", func(path string, d fs.DirEntry, err error) error { path = strings.TrimPrefix(path, "testdata/") diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 5c385d228..a6825d1fd 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -1138,7 +1138,8 @@ func (h *Installer) enableAddon(addon *InstallPackage) error { if !h.skipVersionValidate { err = checkAddonVersionMeetRequired(h.ctx, addon.SystemRequirements, h.cli, h.dc) if err != nil { - return VersionUnMatchError{addonName: addon.Name, err: err} + version := h.getAddonVersionMeetSystemRequirement(addon.Name) + return VersionUnMatchError{addonName: addon.Name, err: err, userSelectedAddonVersion: addon.Version, availableVersion: version} } } @@ -1380,6 +1381,27 @@ func (h *Installer) continueOrRestartWorkflow() error { return nil } +// getAddonVersionMeetSystemRequirement return the addon's latest version which meet the system requirements +func (h *Installer) getAddonVersionMeetSystemRequirement(addonName string) string { + if h.r != nil && IsVersionRegistry(*h.r) { + versionedRegistry := BuildVersionedRegistry(h.r.Name, h.r.Helm.URL, &common.HTTPOption{ + Username: h.r.Helm.Username, + Password: h.r.Helm.Password, + }) + versions, err := versionedRegistry.GetAddonAvailableVersion(addonName) + if err != nil { + return "" + } + for _, version := range versions { + req := LoadSystemRequirements(version.Annotations["system"]) + if checkAddonVersionMeetRequired(h.ctx, req, h.cli, h.dc) == nil { + return version.Version + } + } + } + return "" +} + func addOwner(child *unstructured.Unstructured, app *v1beta1.Application) { child.SetOwnerReferences(append(child.GetOwnerReferences(), *metav1.NewControllerRef(app, v1beta1.ApplicationKindVersionKind))) diff --git a/pkg/addon/addon_test.go b/pkg/addon/addon_test.go index 6b6888d82..698bd67b5 100644 --- a/pkg/addon/addon_test.go +++ b/pkg/addon/addon_test.go @@ -22,6 +22,7 @@ import ( "encoding/xml" "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -113,6 +114,29 @@ var ossHandler http.HandlerFunc = func(rw http.ResponseWriter, req *http.Request } } +var helmHandler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) { + switch { + case strings.Contains(request.URL.Path, "index.yaml"): + files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/index.yaml") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + case strings.Contains(request.URL.Path, "fluxcd-1.0.0.tgz"): + files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + case strings.Contains(request.URL.Path, "fluxcd-2.0.0.tgz"): + files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + } +} + var ctx = context.Background() func testReaderFunc(t *testing.T, reader AsyncReader) { @@ -473,6 +497,22 @@ func TestGetAddonStatus4Observability(t *testing.T) { assert.Equal(t, addonStatus.AddonPhase, enabled) } +func TestGetAddonVersionMeetSystemRequirement(t *testing.T) { + server := httptest.NewServer(helmHandler) + defer server.Close() + i := &Installer{ + r: &Registry{ + Helm: &HelmSource{ + URL: server.URL, + }, + }, + } + version := i.getAddonVersionMeetSystemRequirement("fluxcd-no-requirements") + assert.Equal(t, version, "1.0.0") + version = i.getAddonVersionMeetSystemRequirement("not-exist") + assert.Equal(t, version, "") +} + var baseAddon = InstallPackage{ Meta: Meta{ Name: "test-render-cue-definition-addon", diff --git a/pkg/addon/error.go b/pkg/addon/error.go index 6fa53473b..06ecef735 100644 --- a/pkg/addon/error.go +++ b/pkg/addon/error.go @@ -55,8 +55,24 @@ func WrapErrRateLimit(err error) error { type VersionUnMatchError struct { err error addonName string + // userSelectedAddonVersion is the version of the addon which is selected to install by user + userSelectedAddonVersion string + // availableVersion is the latest available addon's version which suits system requirements + availableVersion string +} + +// GetAvailableVersion load addon's available version from the err +func (v VersionUnMatchError) GetAvailableVersion() (string, error) { + if v.availableVersion == "" { + return "", fmt.Errorf("%s don't exist available version meet system requirement", v.addonName) + } + return v.availableVersion, nil } func (v VersionUnMatchError) Error() string { - return fmt.Sprintf("addon %s system requirement miss match: %v", v.addonName, v.err) + if v.availableVersion != "" { + return fmt.Sprintf("fail to install %s version of %s, because %s.\nInstall %s(v%s) which is the latest version that suits current version requirements", v.userSelectedAddonVersion, v.addonName, v.err, v.addonName, v.availableVersion) + } + return fmt.Sprintf("fail to install %s version of %s, because %s", v.userSelectedAddonVersion, v.addonName, v.err) + } diff --git a/pkg/addon/error_test.go b/pkg/addon/error_test.go new file mode 100644 index 000000000..05709d372 --- /dev/null +++ b/pkg/addon/error_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package addon + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestError(t *testing.T) { + err := &VersionUnMatchError{} + assert.False(t, strings.Contains(err.Error(), "which is the latest version that suits current version requirements")) + err = &VersionUnMatchError{availableVersion: "1.0.0"} + assert.Contains(t, err.Error(), "which is the latest version that suits current version requirements") +} + +func TestGetAvailableVersion(t *testing.T) { + unMatchErr := &VersionUnMatchError{availableVersion: "1.0.0"} + version, err := unMatchErr.GetAvailableVersion() + assert.Empty(t, err) + assert.Equal(t, version, "1.0.0") + + unMatchErr = &VersionUnMatchError{} + version, err = unMatchErr.GetAvailableVersion() + assert.NotEmpty(t, err) + assert.Equal(t, version, "") +} diff --git a/pkg/addon/testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz b/pkg/addon/testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..640c145be02d8c4197bc45abd34d84f694e72d7e GIT binary patch literal 26631 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 Jz<8m+{tu>yV7&kU literal 0 HcmV?d00001 diff --git a/pkg/addon/testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz b/pkg/addon/testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..4da6d1050709905c5e3bad3825fef48b79270b51 GIT binary patch literal 26298 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.3.0; kubernetes>=1.10.0" + - apiVersion: v2 + appVersion: 1.16.0 + description: A Helm chart for Kubernetes + name: fluxcd + type: application + urls: + - http://127.0.0.1:18083/multi/fluxcd-2.0.0.tgz + version: 2.0.0 + annotations: + system: "vela>=1.4.0; kubernetes>=1.20.0" + fluxcd-no-requirements: + - apiVersion: v2 + appVersion: 1.16.0 + description: A Helm chart for Kubernetes + name: fluxcd + type: application + urls: + - http://127.0.0.1:18083/multi/fluxcd-1.0.0.tgz + version: 1.0.0 + annotations: +generated: "2022-06-15T13:17:04.733573+08:00" \ No newline at end of file diff --git a/pkg/addon/versioned_registry.go b/pkg/addon/versioned_registry.go index 6cf3746ef..f22d7cd8c 100644 --- a/pkg/addon/versioned_registry.go +++ b/pkg/addon/versioned_registry.go @@ -20,18 +20,22 @@ import ( "bytes" "context" "fmt" - + "regexp" "sort" "github.com/Masterminds/semver/v3" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/repo" "github.com/oam-dev/kubevela/pkg/apiserver/utils/log" "github.com/oam-dev/kubevela/pkg/utils" "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/pkg/utils/helm" +) - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/repo" +const ( + // patternSystemRequirement is the regex pattern of loading system requirements of the addon + patternSystemRequirement = `vela(.*\d+\.\d+\.\d+);\s?kubernetes(.*\d+\.\d+\.\d+)` ) // VersionedRegistry is the interface of support version registry @@ -40,6 +44,7 @@ type VersionedRegistry interface { GetAddonUIData(ctx context.Context, addonName, version string) (*UIData, error) GetAddonInstallPackage(ctx context.Context, addonName, version string) (*InstallPackage, error) GetDetailedAddon(ctx context.Context, addonName, version string) (*WholeAddonPackage, error) + GetAddonAvailableVersion(addonName string) ([]*repo.ChartVersion, error) } // BuildVersionedRegistry is build versioned addon registry @@ -99,6 +104,11 @@ func (i *versionedRegistry) GetDetailedAddon(ctx context.Context, addonName, ver return wholePackage, nil } +// GetAddonAvailableVersion will return all available versions of the addon which is loaded from the registry, and the version are sorted from last to first +func (i versionedRegistry) GetAddonAvailableVersion(addonName string) ([]*repo.ChartVersion, error) { + return i.loadAddonVersions(addonName) +} + func (i *versionedRegistry) resolveAddonListFromIndex(repoName string, index *repo.IndexFile) []*UIData { var res []*UIData for addonName, versions := range index.Entries { @@ -157,11 +167,25 @@ func (i versionedRegistry) loadAddon(ctx context.Context, name, version string) } addonPkg.AvailableVersions = availableVersions addonPkg.RegistryName = i.name + addonPkg.Meta.SystemRequirements = LoadSystemRequirements(addonVersion.Annotations["system"]) return addonPkg, nil } return nil, fmt.Errorf("cannot fetch addon package") } +// loadAddonVersions Load all available versions of the addon +func (i versionedRegistry) loadAddonVersions(addonName string) ([]*repo.ChartVersion, error) { + versions, err := i.h.ListVersions(i.url, addonName, false, i.Opts) + if err != nil { + return nil, err + } + if len(versions) == 0 { + return nil, ErrNotExist + } + sort.Sort(sort.Reverse(versions)) + return versions, nil +} + func loadAddonPackage(addonName string, files []*loader.BufferedFile) (*WholeAddonPackage, error) { mr := MemoryReader{Name: addonName, Files: files} metas, err := mr.ListAddonMeta() @@ -212,3 +236,21 @@ func chooseVersion(specifiedVersion string, versions []*repo.ChartVersion) (*rep } return addonVersion, availableVersions } + +// LoadSystemRequirements load the system version requirements from the addon's meta file +func LoadSystemRequirements(requirements string) *SystemRequirements { + if len(requirements) == 0 { + return nil + } + regexReq := regexp.MustCompile(patternSystemRequirement) + matched := regexReq.FindStringSubmatch(requirements) + if len(matched) < 3 { + return nil + } + velaReq, k8sReq := matched[1], matched[2] + req := &SystemRequirements{ + VelaVersion: velaReq, + KubernetesVersion: k8sReq, + } + return req +} diff --git a/pkg/addon/versioned_registry_test.go b/pkg/addon/versioned_registry_test.go index 6ae1a2713..68a83c19b 100644 --- a/pkg/addon/versioned_registry_test.go +++ b/pkg/addon/versioned_registry_test.go @@ -23,6 +23,7 @@ import ( "io/ioutil" "log" "net/http" + "net/http/httptest" "strings" "testing" "time" @@ -30,15 +31,17 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/repo" - "github.com/oam-dev/kubevela/pkg/utils/common" - "github.com/stretchr/testify/assert" + + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/utils/helm" ) func TestVersionRegistry(t *testing.T) { go func() { http.HandleFunc("/", versionedHandler) http.HandleFunc("/authReg", basicAuthVersionedHandler) + http.HandleFunc("/multi/", multiVersionHandler) err := http.ListenAndServe(fmt.Sprintf(":%d", 18083), nil) if err != nil { log.Fatal("Setup server error:", err) @@ -98,6 +101,38 @@ func TestVersionRegistry(t *testing.T) { assert.NotEmpty(t, addonWholePackage.RegistryName) testListUIData(t) + mr := BuildVersionedRegistry("multiversion-helm-repo", "http://127.0.0.1:18083/multi", nil) + addons, err = mr.ListAddon() + assert.NoError(t, err) + assert.Equal(t, len(addons), 2) + + addonUIData, err = mr.GetAddonUIData(context.Background(), "fluxcd", "2.0.0") + assert.NoError(t, err) + assert.NotEmpty(t, addonUIData.Definitions) + assert.NotEmpty(t, addonUIData.Icon) + assert.Equal(t, addonUIData.Version, "2.0.0") + + addonsInstallPackage, err = mr.GetAddonInstallPackage(context.Background(), "fluxcd", "1.0.0") + assert.NoError(t, err) + assert.NotEmpty(t, addonsInstallPackage) + assert.NotEmpty(t, addonsInstallPackage.YAMLTemplates) + assert.NotEmpty(t, addonsInstallPackage.DefSchemas) + assert.NotEmpty(t, addonsInstallPackage.SystemRequirements.VelaVersion, "1.3.0") + assert.NotEmpty(t, addonsInstallPackage.SystemRequirements.KubernetesVersion, "1.10.0") + + addonWholePackage, err = mr.GetDetailedAddon(context.Background(), "fluxcd", "1.0.0") + assert.NoError(t, err) + assert.NotEmpty(t, addonWholePackage) + assert.NotEmpty(t, addonWholePackage.YAMLTemplates) + assert.NotEmpty(t, addonWholePackage.DefSchemas) + assert.NotEmpty(t, addonWholePackage.RegistryName) + assert.Equal(t, addonWholePackage.RegistryName, "multiversion-helm-repo") + + version, err := mr.GetAddonAvailableVersion("fluxcd") + assert.NoError(t, err) + assert.Equal(t, len(version), 2) + assert.Equal(t, addonWholePackage.SystemRequirements.VelaVersion, ">=1.3.0") + assert.Equal(t, addonWholePackage.SystemRequirements.KubernetesVersion, ">=1.10.0") } @@ -172,3 +207,66 @@ var basicAuthVersionedHandler http.HandlerFunc = func(writer http.ResponseWriter writer.Write(files) } } + +var multiVersionHandler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) { + switch { + case strings.Contains(request.URL.Path, "index.yaml"): + files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/index.yaml") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + case strings.Contains(request.URL.Path, "fluxcd-1.0.0.tgz"): + files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + case strings.Contains(request.URL.Path, "fluxcd-2.0.0.tgz"): + files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + } +} + +func TestLoadSystemRequirements(t *testing.T) { + req := LoadSystemRequirements("vela>=1.3.0; kubernetes>=1.10.0") + assert.Equal(t, req.VelaVersion, ">=1.3.0") + assert.Equal(t, req.KubernetesVersion, ">=1.10.0") + + req = LoadSystemRequirements("") + assert.Empty(t, req) + + req = LoadSystemRequirements("&&&%%") + assert.Empty(t, req) + + req = LoadSystemRequirements("vela>=; kubernetes>=1.10.0") + assert.Empty(t, req) +} + +func TestLoadAddonVersions(t *testing.T) { + server := httptest.NewServer(multiVersionHandler) + defer server.Close() + mr := &versionedRegistry{ + name: "multiversion-helm-repo", + url: server.URL, + h: helm.NewHelperWithCache(), + Opts: nil, + } + versions, err := mr.loadAddonVersions("not-exist") + assert.Error(t, err) + assert.Equal(t, err, ErrNotExist) + assert.Equal(t, len(versions), 0) + + mr = &versionedRegistry{ + name: "multiversion-helm-repo", + url: server.URL, + h: helm.NewHelperWithCache(), + Opts: nil, + } + versions, err = mr.loadAddonVersions("not-exist") + assert.Error(t, err) + assert.Equal(t, len(versions), 0) +} diff --git a/references/cli/addon.go b/references/cli/addon.go index 9cd2c5607..f8ad3ed7f 100644 --- a/references/cli/addon.go +++ b/references/cli/addon.go @@ -26,25 +26,21 @@ import ( "time" "github.com/fatih/color" - "k8s.io/client-go/discovery" - - "helm.sh/helm/v3/pkg/strvals" - - "github.com/oam-dev/kubevela/pkg/apiserver/domain/service" - "github.com/oam-dev/kubevela/pkg/oam" - - "k8s.io/client-go/rest" - "github.com/gosuri/uitable" "github.com/pkg/errors" "github.com/spf13/cobra" + "helm.sh/helm/v3/pkg/strvals" types2 "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" pkgaddon "github.com/oam-dev/kubevela/pkg/addon" + "github.com/oam-dev/kubevela/pkg/apiserver/domain/service" + "github.com/oam-dev/kubevela/pkg/oam" addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/apply" "github.com/oam-dev/kubevela/pkg/utils/common" @@ -518,10 +514,21 @@ func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.Dis if errors.Is(err, pkgaddon.ErrNotExist) { continue } - if err != nil { - if errors.As(err, &pkgaddon.VersionUnMatchError{}) { - return fmt.Errorf("%w\nyou can try another version by command: \"vela addon enable %s --version \" ", err, name) + if unMatchErr := new(pkgaddon.VersionUnMatchError); errors.As(err, unMatchErr) { + // Get available version of the addon + availableVersion, err := unMatchErr.GetAvailableVersion() + if err != nil { + return err } + input := NewUserInput() + if input.AskBool(unMatchErr.Error(), &UserInputOptions{AssumeYes: false}) { + err = pkgaddon.EnableAddon(ctx, name, availableVersion, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil) + return err + } + // The user does not agree to use the version provided by us + return fmt.Errorf("you can try another version by command: \"vela addon enable %s --version \" ", name) + } + if err != nil { return err } if err = waitApplicationRunning(k8sClient, name); err != nil { diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index 2ef04beff..774bdb2d7 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -35,7 +35,7 @@ var _ = Describe("Test addon rest api", func() { defer resp.Body.Close() var addonRegistry apisv1.ListAddonRegistryResponse Expect(decodeResponseBody(resp, &addonRegistry)).Should(Succeed()) - Expect(len(addonRegistry.Registries)).Should(BeEquivalentTo(1)) + Expect(len(addonRegistry.Registries)).Should(BeEquivalentTo(2)) }) It("add addon registry", func() { @@ -55,7 +55,7 @@ var _ = Describe("Test addon rest api", func() { resp := get("/addon_registries") var addonRegistry apisv1.ListAddonRegistryResponse Expect(decodeResponseBody(resp, &addonRegistry)).Should(Succeed()) - Expect(len(addonRegistry.Registries)).Should(BeEquivalentTo(2)) + Expect(len(addonRegistry.Registries)).Should(BeEquivalentTo(3)) }) It("update an addon registry", func() { @@ -74,8 +74,8 @@ var _ = Describe("Test addon rest api", func() { resp := get("/addon_registries") var addonRegistry apisv1.ListAddonRegistryResponse Expect(decodeResponseBody(resp, &addonRegistry)).Should(Succeed()) - Expect(len(addonRegistry.Registries)).Should(BeEquivalentTo(2)) - Expect(addonRegistry.Registries[1].Git.URL).Should(BeEquivalentTo("github.com/another-path")) + Expect(len(addonRegistry.Registries)).Should(BeEquivalentTo(3)) + Expect(addonRegistry.Registries[2].Git.URL).Should(BeEquivalentTo("github.com/another-path")) }) It("delete an addon registry", func() {