From 161d2646cb8431ef96ecf5ece27a187eb2367124 Mon Sep 17 00:00:00 2001 From: wyike Date: Mon, 28 Mar 2022 21:25:38 +0800 Subject: [PATCH] Feat: helm repo as addon registry to support addon's multi-version (#3523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * versioned registry impl add more test Signed-off-by: 楚岳 * fix ci Signed-off-by: 楚岳 Signed-off-by: 楚岳 fix ui fix fix fix modify addon registry --- .../vela-core/templates/addon_registry.yaml | 6 +- pkg/addon/addon.go | 52 ++++-- pkg/addon/addon_test.go | 4 +- pkg/addon/cache.go | 142 +++++++++++---- pkg/addon/cache_test.go | 33 ++++ pkg/addon/helper.go | 16 +- pkg/addon/memory_reader_test.go | 71 ++++++++ pkg/addon/reader_gitee_test.go | 1 - pkg/addon/reader_github_test.go | 1 - pkg/addon/reader_memory.go | 60 ++++++ pkg/addon/registry.go | 1 + pkg/addon/source.go | 5 + pkg/addon/testdata/helm-repo/fluxcd-1.0.0.tgz | Bin 0 -> 26631 bytes pkg/addon/testdata/helm-repo/index.yaml | 15 ++ pkg/addon/type.go | 15 +- pkg/addon/utils.go | 39 ++-- pkg/addon/versioned_registry.go | 172 ++++++++++++++++++ pkg/addon/versioned_registry_test.go | 76 ++++++++ pkg/apiserver/rest/apis/v1/types.go | 15 +- pkg/apiserver/rest/usecase/addon.go | 30 +-- pkg/apiserver/rest/webservice/addon.go | 4 +- pkg/oam/labels.go | 3 + pkg/utils/helm/helm_helper.go | 11 +- references/cli/addon-registry.go | 16 +- references/cli/addon.go | 69 +++++-- 25 files changed, 729 insertions(+), 128 deletions(-) create mode 100644 pkg/addon/cache_test.go create mode 100644 pkg/addon/memory_reader_test.go create mode 100644 pkg/addon/reader_memory.go create mode 100644 pkg/addon/testdata/helm-repo/fluxcd-1.0.0.tgz create mode 100644 pkg/addon/testdata/helm-repo/index.yaml create mode 100644 pkg/addon/versioned_registry.go create mode 100644 pkg/addon/versioned_registry_test.go diff --git a/charts/vela-core/templates/addon_registry.yaml b/charts/vela-core/templates/addon_registry.yaml index b0ba186c2..2f4232788 100644 --- a/charts/vela-core/templates/addon_registry.yaml +++ b/charts/vela-core/templates/addon_registry.yaml @@ -7,10 +7,8 @@ data: registries: '{ "KubeVela":{ "name": "KubeVela", - "oss": { - "end_point": "https://addons.kubevela.net", - "bucket": "", - "path": "" + "helm": { + "url": "https://addons.kubevela.net", } } }' diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 0f23b1525..a02d8fa5e 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -286,6 +286,7 @@ func GetUIDataFromReader(r AsyncReader, meta *SourceMeta, opt ListOptions) (*UID return nil, fmt.Errorf("fail to generate openAPIschema for addon %s : %w", meta.Name, err) } } + addon.AvailableVersions = []string{addon.Version} return addon, nil } @@ -595,6 +596,7 @@ func formatAppFramework(addon *InstallPackage) *v1beta1.Application { app.Labels = make(map[string]string) } app.Labels[oam.LabelAddonName] = addon.Name + app.Labels[oam.LabelAddonVersion] = addon.Version return app } @@ -1051,26 +1053,37 @@ func (h *Installer) enableAddon(addon *InstallPackage) error { return nil } -func (h *Installer) loadInstallPackage(name string) (*InstallPackage, error) { - metas, err := h.getAddonMeta() - if err != nil { - return nil, errors.Wrap(err, "fail to get addon meta") +func (h *Installer) loadInstallPackage(name, version string) (*InstallPackage, error) { + var installPackage *InstallPackage + var err error + if !IsVersionRegistry(*h.r) { + metas, err := h.getAddonMeta() + if err != nil { + return nil, errors.Wrap(err, "fail to get addon meta") + } + + meta, ok := metas[name] + if !ok { + return nil, ErrNotExist + } + var uiData *UIData + uiData, err = h.cache.GetUIData(*h.r, name, version) + if err != nil { + return nil, err + } + // enable this addon if it's invisible + installPackage, err = h.r.GetInstallPackage(&meta, uiData) + if err != nil { + return nil, errors.Wrap(err, "fail to find dependent addon in source repository") + } + } else { + versionedRegistry := BuildVersionedRegistry(h.r.Name, h.r.Helm.URL) + installPackage, err = versionedRegistry.GetAddonInstallPackage(context.Background(), name, version) + if err != nil { + return nil, err + } } - meta, ok := metas[name] - if !ok { - return nil, ErrNotExist - } - var uiData *UIData - uiData, err = h.cache.GetUIData(*h.r, name) - if err != nil { - return nil, err - } - // enable this addon if it's invisible - installPackage, err := h.r.GetInstallPackage(&meta, uiData) - if err != nil { - return nil, errors.Wrap(err, "fail to find dependent addon in source repository") - } return installPackage, nil } @@ -1098,7 +1111,8 @@ func (h *Installer) installDependency(addon *InstallPackage) error { if !apierrors.IsNotFound(err) { return err } - depAddon, err := h.loadInstallPackage(dep.Name) + // always install addon's latest version + depAddon, err := h.loadInstallPackage(dep.Name, "") if err != nil { return err } diff --git a/pkg/addon/addon_test.go b/pkg/addon/addon_test.go index bb72e6b6a..4f1c35dbe 100644 --- a/pkg/addon/addon_test.go +++ b/pkg/addon/addon_test.go @@ -552,7 +552,7 @@ func TestRenderApp4Observability(t *testing.T) { }, }, args: map[string]interface{}{}, - application: `{"kind":"Application","apiVersion":"core.oam.dev/v1beta1","metadata":{"name":"addon-observability","namespace":"vela-system","creationTimestamp":null,"labels":{"addons.oam.dev/name":"observability"}},"spec":{"components":[],"policies":[{"name":"domain","type":"env-binding","properties":{"envs":null}}],"workflow":{"steps":[{"name":"deploy-control-plane","type":"apply-application"}]}},"status":{}}`, + application: `{"kind":"Application","apiVersion":"core.oam.dev/v1beta1","metadata":{"name":"addon-observability","namespace":"vela-system","creationTimestamp":null,"labels":{"addons.oam.dev/name":"observability","addons.oam.dev/version":""}},"spec":{"components":[],"policies":[{"name":"domain","type":"env-binding","properties":{"envs":null}}],"workflow":{"steps":[{"name":"deploy-control-plane","type":"apply-application"}]}},"status":{}}`, }, } for _, tc := range testcases { @@ -599,7 +599,7 @@ func TestRenderApp4ObservabilityWithK8sData(t *testing.T) { }, }, args: map[string]interface{}{}, - application: `{"kind":"Application","apiVersion":"core.oam.dev/v1beta1","metadata":{"name":"addon-observability","namespace":"vela-system","creationTimestamp":null,"labels":{"addons.oam.dev/name":"observability"}},"spec":{"components":[],"policies":[{"name":"domain","type":"env-binding","properties":{"envs":[{"name":"test-secret","placement":{"clusterSelector":{"name":"test-secret"}}}]}}],"workflow":{"steps":[{"name":"deploy-control-plane","type":"apply-application-in-parallel"},{"name":"test-secret","type":"deploy2env","properties":{"env":"test-secret","parallel":true,"policy":"domain"}}]}},"status":{}}`, + application: `{"kind":"Application","apiVersion":"core.oam.dev/v1beta1","metadata":{"name":"addon-observability","namespace":"vela-system","creationTimestamp":null,"labels":{"addons.oam.dev/name":"observability","addons.oam.dev/version":""}},"spec":{"components":[],"policies":[{"name":"domain","type":"env-binding","properties":{"envs":[{"name":"test-secret","placement":{"clusterSelector":{"name":"test-secret"}}}]}}],"workflow":{"steps":[{"name":"deploy-control-plane","type":"apply-application-in-parallel"},{"name":"test-secret","type":"deploy2env","properties":{"env":"test-secret","parallel":true,"policy":"domain"}}]}},"status":{}}`, }, } for _, tc := range testcases { diff --git a/pkg/addon/cache.go b/pkg/addon/cache.go index 6a29b6336..678b13eeb 100644 --- a/pkg/addon/cache.go +++ b/pkg/addon/cache.go @@ -43,6 +43,8 @@ type Cache struct { registry map[string]Registry + versionedUIData map[string]map[string]*UIData + mutex *sync.RWMutex ds RegistryDataStore @@ -51,11 +53,12 @@ type Cache struct { // NewCache will build a new cache instance func NewCache(ds RegistryDataStore) *Cache { return &Cache{ - uiData: make(map[string][]*UIData), - registryMeta: make(map[string]map[string]SourceMeta), - registry: make(map[string]Registry), - mutex: new(sync.RWMutex), - ds: ds, + uiData: make(map[string][]*UIData), + registryMeta: make(map[string]map[string]SourceMeta), + registry: make(map[string]Registry), + versionedUIData: make(map[string]map[string]*UIData), + mutex: new(sync.RWMutex), + ds: ds, } } @@ -80,21 +83,35 @@ func (u *Cache) ListAddonMeta(r Registry) (map[string]SourceMeta, error) { } // GetUIData get addon data for UI display from cache, if cache not found, it will find from source -func (u *Cache) GetUIData(r Registry, addonName string) (*UIData, error) { - addon := u.getCachedUIData(r.Name, addonName) +func (u *Cache) GetUIData(r Registry, addonName, version string) (*UIData, error) { + addon := u.getCachedUIData(r, addonName, version) if addon != nil { return addon, nil } var err error - registryMeta, err := u.ListAddonMeta(r) - if err != nil { - return nil, err + if !IsVersionRegistry(r) { + registryMeta, err := u.ListAddonMeta(r) + if err != nil { + return nil, err + } + meta, ok := registryMeta[addonName] + if !ok { + return nil, ErrNotExist + } + addon, err = r.GetUIData(&meta, UIMetaOptions) + if err != nil { + return nil, err + } + } else { + versionedRegistry := BuildVersionedRegistry(r.Name, r.Helm.URL) + addon, err = versionedRegistry.GetAddonUIData(context.Background(), addonName, version) + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", r.Name, err) + return nil, err + } } - meta, ok := registryMeta[addonName] - if !ok { - return nil, ErrNotExist - } - return r.GetUIData(&meta, UIMetaOptions) + + return addon, nil } // ListUIData will always list UIData from cache first, if not exist, read from source. @@ -104,24 +121,40 @@ func (u *Cache) ListUIData(r Registry) ([]*UIData, error) { if listAddons != nil { return listAddons, nil } - addonMeta, err := u.ListAddonMeta(r) - if err != nil { - return nil, err - } - listAddons, err = r.ListUIData(addonMeta, UIMetaOptions) - if err != nil { - return nil, fmt.Errorf("fail to get addons from registry %s, %w", r.Name, err) + if !IsVersionRegistry(r) { + addonMeta, err := u.ListAddonMeta(r) + if err != nil { + return nil, err + } + listAddons, err = r.ListUIData(addonMeta, UIMetaOptions) + if err != nil { + return nil, fmt.Errorf("fail to get addons from registry %s, %w", r.Name, err) + } + } else { + versionedRegistry := BuildVersionedRegistry(r.Name, r.Helm.URL) + listAddons, err = versionedRegistry.ListAddon() + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", r.Name, err) + return nil, err + } } u.putAddonUIData2Cache(r.Name, listAddons) return listAddons, nil } -func (u *Cache) getCachedUIData(registry, addonName string) *UIData { - addons := u.listCachedUIData(registry) - for _, a := range addons { - if a.Name == addonName { - return a +func (u *Cache) getCachedUIData(registry Registry, addonName, version string) *UIData { + if !IsVersionRegistry(registry) { + addons := u.listCachedUIData(registry.Name) + for _, a := range addons { + if a.Name == addonName { + return a + } } + } else { + if len(version) == 0 { + version = "latest" + } + return u.versionedUIData[registry.Name][fmt.Sprintf("%s-%s", addonName, version)] } return nil } @@ -201,6 +234,20 @@ func (u *Cache) putRegistry2Cache(registry []Registry) { } } +func (u *Cache) putVersionedUIData2Cache(registryName, addonName, version string, uiData *UIData) { + if u == nil { + return + } + + u.mutex.Lock() + defer u.mutex.Unlock() + + if u.versionedUIData[registryName] == nil { + u.versionedUIData[registryName] = make(map[string]*UIData) + } + u.versionedUIData[registryName][fmt.Sprintf("%s-%s", addonName, version)] = uiData +} + func (u *Cache) discoverAndRefreshRegistry() { registries, err := u.ds.ListRegistries(context.Background()) if err != nil { @@ -210,17 +257,36 @@ func (u *Cache) discoverAndRefreshRegistry() { u.putRegistry2Cache(registries) for _, r := range registries { - registryMeta, err := r.ListAddonMeta() - if err != nil { - log.Logger.Errorf("fail to list registry %s metadata, %v", r.Name, err) - continue + if !IsVersionRegistry(r) { + registryMeta, err := r.ListAddonMeta() + if err != nil { + log.Logger.Errorf("fail to list registry %s metadata, %v", r.Name, err) + continue + } + u.putAddonMeta2Cache(r.Name, registryMeta) + uiData, err := r.ListUIData(registryMeta, UIMetaOptions) + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", r.Name, err) + continue + } + u.putAddonUIData2Cache(r.Name, uiData) + } else { + versionedRegistry := BuildVersionedRegistry(r.Name, r.Helm.URL) + uiDatas, err := versionedRegistry.ListAddon() + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", r.Name, err) + continue + } + for _, addon := range uiDatas { + uiData, err := versionedRegistry.GetAddonUIData(context.Background(), addon.Name, addon.Version) + if err != nil { + log.Logger.Errorf("fail to get addon from registry %s, addon %s version %s for cache updating, %v", addon.Name, r.Name, err) + continue + } + u.putVersionedUIData2Cache(r.Name, addon.Name, addon.Version, uiData) + // we also no version key, if use get addonUIData without version will return this vale as latest data. + u.putVersionedUIData2Cache(r.Name, addon.Name, "latest", uiData) + } } - u.putAddonMeta2Cache(r.Name, registryMeta) - uiData, err := r.ListUIData(registryMeta, UIMetaOptions) - if err != nil { - log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", r.Name, err) - continue - } - u.putAddonUIData2Cache(r.Name, uiData) } } diff --git a/pkg/addon/cache_test.go b/pkg/addon/cache_test.go new file mode 100644 index 000000000..44ab7e180 --- /dev/null +++ b/pkg/addon/cache_test.go @@ -0,0 +1,33 @@ +/* +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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPutVersionedUIData2cache(t *testing.T) { + uiData := UIData{Meta: Meta{Name: "fluxcd", Icon: "test.com/fluxcd.png", Version: "1.0.0"}} + u := NewCache(nil) + u.putVersionedUIData2Cache("helm-repo", "fluxcd", "1.0.0", &uiData) + assert.NotEmpty(t, u.versionedUIData) + assert.NotEmpty(t, u.versionedUIData["helm-repo"]) + assert.NotEmpty(t, u.versionedUIData["helm-repo"]["fluxcd-1.0.0"]) + assert.Equal(t, u.versionedUIData["helm-repo"]["fluxcd-1.0.0"].Name, "fluxcd") +} diff --git a/pkg/addon/helper.go b/pkg/addon/helper.go index c1112127a..6363d1d4c 100644 --- a/pkg/addon/helper.go +++ b/pkg/addon/helper.go @@ -21,10 +21,7 @@ import ( "encoding/json" "fmt" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "k8s.io/client-go/discovery" - "k8s.io/klog/v2" v1 "k8s.io/api/core/v1" @@ -34,8 +31,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" commontypes "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" "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/utils/apply" ) @@ -53,9 +52,9 @@ const ( ) // EnableAddon will enable addon with dependency check, source is where addon from. -func EnableAddon(ctx context.Context, name string, cli client.Client, discoveryClient *discovery.DiscoveryClient, apply apply.Applicator, config *rest.Config, r Registry, args map[string]interface{}, cache *Cache) error { +func EnableAddon(ctx context.Context, name string, version string, cli client.Client, discoveryClient *discovery.DiscoveryClient, apply apply.Applicator, config *rest.Config, r Registry, args map[string]interface{}, cache *Cache) error { h := NewAddonInstaller(ctx, cli, discoveryClient, apply, config, &r, args, cache) - pkg, err := h.loadInstallPackage(name) + pkg, err := h.loadInstallPackage(name, version) if err != nil { return err } @@ -172,11 +171,11 @@ func GetAddonStatus(ctx context.Context, cli client.Client, name string) (Status } return Status{AddonPhase: enabled, AppStatus: &app.Status, Clusters: clusters}, nil } - return Status{AddonPhase: enabled, AppStatus: &app.Status}, nil + return Status{AddonPhase: enabled, AppStatus: &app.Status, InstalledVersion: app.GetLabels()[oam.LabelAddonVersion]}, nil case commontypes.ApplicationDeleting: return Status{AddonPhase: disabling, AppStatus: &app.Status}, nil default: - return Status{AddonPhase: enabling, AppStatus: &app.Status}, nil + return Status{AddonPhase: enabling, AppStatus: &app.Status, InstalledVersion: app.GetLabels()[oam.LabelAddonVersion]}, nil } } @@ -235,5 +234,6 @@ type Status struct { AddonPhase string AppStatus *commontypes.AppStatus // the status of multiple clusters - Clusters map[string]map[string]interface{} `json:"clusters,omitempty"` + Clusters map[string]map[string]interface{} `json:"clusters,omitempty"` + InstalledVersion string } diff --git a/pkg/addon/memory_reader_test.go b/pkg/addon/memory_reader_test.go new file mode 100644 index 000000000..aea58b283 --- /dev/null +++ b/pkg/addon/memory_reader_test.go @@ -0,0 +1,71 @@ +/* +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 ( + "testing" + + "github.com/stretchr/testify/assert" + "helm.sh/helm/v3/pkg/chart/loader" +) + +var files = []*loader.BufferedFile{ + { + Name: "metadata.yaml", + Data: []byte(`name: test-helm-addon +version: 1.0.0 +description: This is a addon for test when install addon from helm repo +icon: https://www.terraform.io/assets/images/logo-text-8c3ba8a6.svg +url: https://terraform.io/ + +tags: [] + +deployTo: + control_plane: true + runtime_cluster: false + +dependencies: [] + +invisible: false`), + }, + { + Name: "/resources/parameter.cue", + Data: []byte(`parameter: { + // test wrong parameter + example: *"default" +}`), + }, +} + +func TestMemoryReader(t *testing.T) { + m := MemoryReader{ + Name: "fluxcd", + Files: files, + } + + meta, err := m.ListAddonMeta() + assert.NoError(t, err) + assert.Equal(t, len(meta["fluxcd"].Items), 2) + + metaFile, err := m.ReadFile("metadata.yaml") + assert.NoError(t, err) + assert.NotEmpty(t, metaFile) + + paramterData, err := m.ReadFile("/resources/parameter.cue") + assert.NoError(t, err) + assert.NotEmpty(t, paramterData) +} diff --git a/pkg/addon/reader_gitee_test.go b/pkg/addon/reader_gitee_test.go index eb49adc6a..2f6f9d02c 100644 --- a/pkg/addon/reader_gitee_test.go +++ b/pkg/addon/reader_gitee_test.go @@ -96,5 +96,4 @@ func TestGiteeReader(t *testing.T) { _, err := r.ReadFile("example/metadata.yaml") assert.NoError(t, err) - testReaderFunc(t, r) } diff --git a/pkg/addon/reader_github_test.go b/pkg/addon/reader_github_test.go index cef9ee73d..1d971c03b 100644 --- a/pkg/addon/reader_github_test.go +++ b/pkg/addon/reader_github_test.go @@ -108,7 +108,6 @@ func TestGitHubReader(t *testing.T) { _, err := r.ReadFile("example/metadata.yaml") assert.NoError(t, err) - testReaderFunc(t, r) } // Int is a helper routine that allocates a new int value diff --git a/pkg/addon/reader_memory.go b/pkg/addon/reader_memory.go new file mode 100644 index 000000000..d9d70fa1c --- /dev/null +++ b/pkg/addon/reader_memory.go @@ -0,0 +1,60 @@ +/* +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 ( + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/chart/loader" +) + +// MemoryReader is async reader for memory data +type MemoryReader struct { + Name string + Files []*loader.BufferedFile + fileData map[string]string +} + +// ListAddonMeta list all metadata of helm repo registry +func (l *MemoryReader) ListAddonMeta() (map[string]SourceMeta, error) { + metas := SourceMeta{Name: l.Name} + for _, f := range l.Files { + metas.Items = append(metas.Items, OSSItem{tp: "file", name: f.Name}) + if l.fileData == nil { + l.fileData = make(map[string]string) + } + l.fileData[f.Name] = string(f.Data) + } + return map[string]SourceMeta{l.Name: metas}, nil +} + +// ReadFile ready file from memory +func (l *MemoryReader) ReadFile(path string) (string, error) { + if file, ok := l.fileData[path]; ok { + return file, nil + } + return l.fileData[strings.TrimPrefix(path, l.Name+"/")], nil +} + +// RelativePath calculate the relative path of one file +func (l *MemoryReader) RelativePath(item Item) string { + if strings.HasPrefix(item.GetName(), l.Name) { + return item.GetName() + } + return filepath.Join(l.Name, item.GetName()) +} diff --git a/pkg/addon/registry.go b/pkg/addon/registry.go index 5456ca308..d07d9d682 100644 --- a/pkg/addon/registry.go +++ b/pkg/addon/registry.go @@ -37,6 +37,7 @@ const registriesKey = "registries" type Registry struct { Name string `json:"name"` + Helm *HelmSource `json:"helm,omitempty"` Git *GitAddonSource `json:"git,omitempty"` OSS *OSSAddonSource `json:"oss,omitempty"` Gitee *GiteeAddonSource `json:"gitee,omitempty"` diff --git a/pkg/addon/source.go b/pkg/addon/source.go index a2872d69b..bdb4843dc 100644 --- a/pkg/addon/source.go +++ b/pkg/addon/source.go @@ -63,6 +63,11 @@ type GiteeAddonSource struct { Token string `json:"token,omitempty"` } +// HelmSource defines the information about the helm repo addon source +type HelmSource struct { + URL string `json:"url,omitempty" validate:"required"` +} + // Item is a partial interface for github.RepositoryContent type Item interface { // GetType return "dir" or "file" diff --git a/pkg/addon/testdata/helm-repo/fluxcd-1.0.0.tgz b/pkg/addon/testdata/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/helm-repo/index.yaml b/pkg/addon/testdata/helm-repo/index.yaml new file mode 100644 index 000000000..295e85662 --- /dev/null +++ b/pkg/addon/testdata/helm-repo/index.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +entries: + fluxcd: + - created: "2022-03-25T21:04:51.244331+08:00" + description: Extended workload to do continuous and progressive delivery + home: https://fluxcd.io + icon: https://raw.githubusercontent.com/fluxcd/flux/master/docs/_files/weave-flux.png + keywords: + - extended_workload + - gitops + name: fluxcd + urls: + - http://127.0.0.1:18083/fluxcd-1.0.0.tgz + version: 1.0.0 +generated: "0001-01-01T00:00:00Z" diff --git a/pkg/addon/type.go b/pkg/addon/type.go index 31aff0b11..f8346115f 100644 --- a/pkg/addon/type.go +++ b/pkg/addon/type.go @@ -37,6 +37,8 @@ type UIData struct { CUEDefinitions []ElementFile `json:"CUEDefinitions"` Parameters string `json:"parameters"` RegistryName string `json:"registryName"` + + AvailableVersions []string `json:"availableVersions"` } // InstallPackage contains all necessary files that can be installed for an addon @@ -47,7 +49,7 @@ type InstallPackage struct { Definitions []ElementFile `json:"definitions"` CUEDefinitions []ElementFile `json:"CUEDefinitions"` // DefSchemas are UI schemas read by VelaUX, it will only be installed in control plane clusters - DefSchemas []ElementFile `json:"def_schemas,omitempty"` + DefSchemas []ElementFile `json:"defSchemas,omitempty"` Parameters string `json:"parameters"` @@ -57,6 +59,17 @@ type InstallPackage struct { AppTemplate *v1beta1.Application `json:"appTemplate"` } +// WholeAddonPackage contains all infos of an addon +type WholeAddonPackage struct { + InstallPackage + + APISchema *openapi3.Schema `json:"schema"` + + // Detail is README.md in an addon + Detail string `json:"detail,omitempty"` + AvailableVersions []string `json:"availableVersions"` +} + // Meta defines the format for a single addon type Meta struct { Name string `json:"name" validate:"required"` diff --git a/pkg/addon/utils.go b/pkg/addon/utils.go index 9e0dafdd1..76893e821 100644 --- a/pkg/addon/utils.go +++ b/pkg/addon/utils.go @@ -153,18 +153,28 @@ func findLegacyAddonDefs(ctx context.Context, k8sClient client.Client, addonName var defObjects []*unstructured.Unstructured for i, registry := range registries { if registry.Name == registryName { - installer := NewAddonInstaller(ctx, k8sClient, nil, nil, config, ®istries[i], nil, nil) - metas, err := installer.getAddonMeta() - if err != nil { - return err + var uiData *UIData + if !IsVersionRegistry(registry) { + installer := NewAddonInstaller(ctx, k8sClient, nil, nil, config, ®istries[i], nil, nil) + metas, err := installer.getAddonMeta() + if err != nil { + return err + } + meta := metas[addonName] + // only fetch definition files from registry. + uiData, err = registry.GetUIData(&meta, UnInstallOptions) + if err != nil { + return errors.Wrapf(err, "cannot fetch addon difinition files from registry") + } + } else { + versionedRegistry := BuildVersionedRegistry(registry.Name, registry.Helm.URL) + uiData, err = versionedRegistry.GetAddonUIData(ctx, addonName, "") + if err != nil { + return errors.Wrapf(err, "cannot fetch addon difinition files from registry") + } } - meta := metas[addonName] - // only fetch definition files from registry. - data, err := registry.GetUIData(&meta, UnInstallOptions) - if err != nil { - return errors.Wrapf(err, "cannot fetch addon difinition files from registry") - } - for _, defYaml := range data.Definitions { + + for _, defYaml := range uiData.Definitions { def, err := renderObject(defYaml) if err != nil { // don't let one error defined definition block whole disable process @@ -172,7 +182,7 @@ func findLegacyAddonDefs(ctx context.Context, k8sClient client.Client, addonName } defObjects = append(defObjects, def) } - for _, cueDef := range data.CUEDefinitions { + for _, cueDef := range uiData.CUEDefinitions { def := definition.Definition{Unstructured: unstructured.Unstructured{}} err := def.FromCUEString(cueDef.Data, config) if err != nil { @@ -209,3 +219,8 @@ func usingAppsInfo(apps []v1beta1.Application) string { res = strings.TrimSuffix(res, ",") + ".Please delete them before disabling the addon." return res } + +// IsVersionRegistry check the repo source if support multi-version addon +func IsVersionRegistry(r Registry) bool { + return r.Helm != nil +} diff --git a/pkg/addon/versioned_registry.go b/pkg/addon/versioned_registry.go new file mode 100644 index 000000000..904dcdc9d --- /dev/null +++ b/pkg/addon/versioned_registry.go @@ -0,0 +1,172 @@ +/* +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 ( + "bytes" + "context" + "fmt" + "sort" + + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/repo" + + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/utils/helm" +) + +// VersionedRegistry is the interface of support version registry +type VersionedRegistry interface { + ListAddon() ([]*UIData, error) + GetAddonUIData(ctx context.Context, addonName, version string) (*UIData, error) + GetAddonInstallPackage(ctx context.Context, addonName, version string) (*InstallPackage, error) +} + +// BuildVersionedRegistry is build versioned addon registry +func BuildVersionedRegistry(name, repoURL string) VersionedRegistry { + return &versionedRegistry{ + name: name, + url: repoURL, + h: helm.NewHelperWithCache(), + } +} + +type versionedRegistry struct { + url string + name string + h *helm.Helper +} + +func (i *versionedRegistry) ListAddon() ([]*UIData, error) { + chartIndex, err := i.h.GetIndexInfo(i.url, false) + if err != nil { + return nil, err + } + return i.resolveAddonListFromIndex(i.name, chartIndex), nil +} + +func (i *versionedRegistry) GetAddonUIData(ctx context.Context, addonName, version string) (*UIData, error) { + wholePackage, err := i.loadAddon(ctx, addonName, version) + if err != nil { + return nil, err + } + return &UIData{ + Meta: wholePackage.Meta, + APISchema: wholePackage.APISchema, + Parameters: wholePackage.Parameters, + Detail: wholePackage.Detail, + Definitions: wholePackage.Definitions, + AvailableVersions: wholePackage.AvailableVersions, + }, nil +} + +func (i *versionedRegistry) GetAddonInstallPackage(ctx context.Context, addonName, version string) (*InstallPackage, error) { + wholePackage, err := i.loadAddon(context.Background(), addonName, version) + if err != nil { + return nil, err + } + return &wholePackage.InstallPackage, nil +} + +func (i *versionedRegistry) resolveAddonListFromIndex(repoName string, index *repo.IndexFile) []*UIData { + var res []*UIData + for addonName, versions := range index.Entries { + if len(versions) == 0 { + continue + } + sort.Sort(sort.Reverse(versions)) + latestVersion := versions[0] + var availableVersions []string + for _, version := range versions { + availableVersions = append(availableVersions, version.Version) + } + o := UIData{Meta: Meta{ + Name: addonName, + Icon: latestVersion.Icon, + Tags: latestVersion.Keywords, + Description: latestVersion.Description, + Version: latestVersion.Version, + }, RegistryName: repoName, AvailableVersions: availableVersions} + res = append(res, &o) + } + return res +} + +func (i versionedRegistry) loadAddon(ctx context.Context, name, version string) (*WholeAddonPackage, error) { + versions, err := i.h.ListVersions(i.url, name, false) + if err != nil { + return nil, err + } + if len(versions) == 0 { + return nil, ErrNotExist + } + var addonVersion *repo.ChartVersion + sort.Sort(sort.Reverse(versions)) + if len(version) == 0 { + // if not specify version will always use latest version + addonVersion = versions[0] + } + var availableVersions []string + for i, v := range versions { + if v.Version == version { + addonVersion = versions[i] + availableVersions = append(availableVersions, v.Version) + } + } + if addonVersion == nil { + return nil, nil + } + for _, chartURL := range addonVersion.URLs { + archive, err := common.HTTPGet(ctx, chartURL) + if err != nil { + continue + } + bufferedFile, err := loader.LoadArchiveFiles(bytes.NewReader(archive)) + if err != nil { + continue + } + addonPkg, err := loadAddonPackage(name, bufferedFile) + if err != nil { + return nil, err + } + addonPkg.AvailableVersions = availableVersions + return addonPkg, nil + } + return nil, fmt.Errorf("cannot fetch addon package") +} + +func loadAddonPackage(addonName string, files []*loader.BufferedFile) (*WholeAddonPackage, error) { + mr := MemoryReader{Name: addonName, Files: files} + metas, err := mr.ListAddonMeta() + if err != nil { + return nil, err + } + meta := metas[addonName] + addonUIData, err := GetUIDataFromReader(&mr, &meta, UIMetaOptions) + if err != nil { + return nil, err + } + installPackage, err := GetInstallPackageFromReader(&mr, &meta, addonUIData) + if err != nil { + return nil, err + } + return &WholeAddonPackage{ + InstallPackage: *installPackage, + Detail: addonUIData.Detail, + APISchema: addonUIData.APISchema, + }, nil +} diff --git a/pkg/addon/versioned_registry_test.go b/pkg/addon/versioned_registry_test.go new file mode 100644 index 000000000..b67926d7c --- /dev/null +++ b/pkg/addon/versioned_registry_test.go @@ -0,0 +1,76 @@ +/* +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 ( + "context" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestVersionRegistry(t *testing.T) { + go func() { + http.HandleFunc("/", versionedHandler) + err := http.ListenAndServe(fmt.Sprintf(":%d", 18083), nil) + if err != nil { + log.Fatal("Setup server error:", err) + } + }() + // wait server setup + time.Sleep(3 * time.Second) + r := BuildVersionedRegistry("helm-repo", "http://127.0.0.1:18083") + addons, err := r.ListAddon() + assert.NoError(t, err) + assert.Equal(t, len(addons), 1) + assert.Equal(t, addons[0].Name, "fluxcd") + assert.Equal(t, len(addons[0].AvailableVersions), 1) + + addonUIData, err := r.GetAddonUIData(context.Background(), "fluxcd", "1.0.0") + assert.NoError(t, err) + assert.NotEmpty(t, addonUIData.Definitions) + assert.NotEmpty(t, addonUIData.Icon) + + addonsInstallPackage, err := r.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) +} + +var versionedHandler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) { + switch { + case strings.Contains(request.URL.Path, "index.yaml"): + files, err := ioutil.ReadFile("./testdata/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/helm-repo/fluxcd-1.0.0.tgz") + if err != nil { + _, _ = writer.Write([]byte(err.Error())) + } + writer.Write(files) + } +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 1fffedb17..0908c5b61 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -75,6 +75,7 @@ type NameAlias struct { // CreateAddonRegistryRequest defines the format for addon registry create request type CreateAddonRegistryRequest struct { Name string `json:"name" validate:"checkname"` + Helm *addon.HelmSource `json:"helm,omitempty"` Git *addon.GitAddonSource `json:"git,omitempty" ` Oss *addon.OSSAddonSource `json:"oss,omitempty"` Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" ` @@ -82,6 +83,7 @@ type CreateAddonRegistryRequest struct { // UpdateAddonRegistryRequest defines the format for addon registry update request type UpdateAddonRegistryRequest struct { + Helm *addon.HelmSource `json:"helm,omitempty"` Git *addon.GitAddonSource `json:"git,omitempty"` Oss *addon.OSSAddonSource `json:"oss,omitempty"` Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" ` @@ -90,6 +92,7 @@ type UpdateAddonRegistryRequest struct { // AddonRegistry defines the format for a single addon registry type AddonRegistry struct { Name string `json:"name" validate:"required"` + Helm *addon.HelmSource `json:"helm,omitempty"` Git *addon.GitAddonSource `json:"git,omitempty"` OSS *addon.OSSAddonSource `json:"oss,omitempty"` Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" ` @@ -106,6 +109,8 @@ type EnableAddonRequest struct { Args map[string]interface{} `json:"args,omitempty"` // Clusters specify the clusters this addon should be installed, if not specified, it will follow the configure in addon metadata.yaml Clusters []string `json:"clusters,omitempty"` + // Version specify the version of addon to enable + Version string `json:"version,omitempty"` } // ListAddonResponse defines the format for addon list response @@ -141,9 +146,10 @@ type DetailAddonResponse struct { UISchema utils.UISchema `json:"uiSchema"` // More details about the addon, e.g. README - Detail string `json:"detail,omitempty"` - Definitions []*AddonDefinition `json:"definitions"` - RegistryName string `json:"registryName,omitempty"` + Detail string `json:"detail,omitempty"` + Definitions []*AddonDefinition `json:"definitions"` + RegistryName string `json:"registryName,omitempty"` + AvailableVersions []string `json:"availableVersions"` } // AddonDefinition is definition an addon can provide @@ -161,7 +167,8 @@ type AddonStatusResponse struct { EnablingProgress *EnablingProgress `json:"enabling_progress,omitempty"` AppStatus common.AppStatus `json:"appStatus,omitempty"` // the status of multiple clusters - Clusters map[string]map[string]interface{} `json:"clusters,omitempty"` + Clusters map[string]map[string]interface{} `json:"clusters,omitempty"` + InstalledVersion string `json:"installedVersion,omitempty"` } // EnablingProgress defines the progress of enabling an addon diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index e64d98c91..f3ad11cd5 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -58,7 +58,7 @@ type AddonHandler interface { ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistry, error) ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) - GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) + GetAddon(ctx context.Context, name string, registry string, version string) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error DisableAddon(ctx context.Context, name string, force bool) error ListEnabledAddon(ctx context.Context) ([]*apis.AddonBaseStatus, error) @@ -82,12 +82,13 @@ func AddonImpl2AddonRes(impl *pkgaddon.UIData) (*apis.DetailAddonResponse, error }) } return &apis.DetailAddonResponse{ - Meta: impl.Meta, - APISchema: impl.APISchema, - UISchema: impl.UISchema, - Detail: impl.Detail, - Definitions: defs, - RegistryName: impl.RegistryName, + Meta: impl.Meta, + APISchema: impl.APISchema, + UISchema: impl.UISchema, + Detail: impl.Detail, + Definitions: defs, + RegistryName: impl.RegistryName, + AvailableVersions: impl.AvailableVersions, }, nil } @@ -134,7 +135,7 @@ type defaultAddonHandler struct { } // GetAddon will get addon information -func (u *defaultAddonHandler) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) { +func (u *defaultAddonHandler) GetAddon(ctx context.Context, name string, registry string, version string) (*apis.DetailAddonResponse, error) { var addon *pkgaddon.UIData var err error if registry == "" { @@ -143,7 +144,7 @@ func (u *defaultAddonHandler) GetAddon(ctx context.Context, name string, registr return nil, err } for _, r := range registries { - addon, err = u.addonRegistryCache.GetUIData(r, name) + addon, err = u.addonRegistryCache.GetUIData(r, name, version) if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { return nil, err } @@ -156,7 +157,7 @@ func (u *defaultAddonHandler) GetAddon(ctx context.Context, name string, registr if err != nil { return nil, err } - addon, err = u.addonRegistryCache.GetUIData(addonRegistry, name) + addon, err = u.addonRegistryCache.GetUIData(addonRegistry, name, version) if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { return nil, err } @@ -195,8 +196,9 @@ func (u *defaultAddonHandler) StatusAddon(ctx context.Context, name string) (*ap Name: name, Phase: apis.AddonPhase(status.AddonPhase), }, - AppStatus: *status.AppStatus, - Clusters: status.Clusters, + InstalledVersion: status.InstalledVersion, + AppStatus: *status.AppStatus, + Clusters: status.Clusters, } if res.Phase != apis.AddonPhaseEnabled { @@ -360,7 +362,7 @@ func (u *defaultAddonHandler) EnableAddon(ctx context.Context, name string, args return err } for _, r := range registries { - err = pkgaddon.EnableAddon(ctx, name, u.kubeClient, u.discoveryClient, u.apply, u.config, r, args.Args, u.addonRegistryCache) + err = pkgaddon.EnableAddon(ctx, name, args.Version, u.kubeClient, u.discoveryClient, u.apply, u.config, r, args.Args, u.addonRegistryCache) if err == nil { return nil } @@ -428,7 +430,7 @@ func (u *defaultAddonHandler) UpdateAddon(ctx context.Context, name string, args } for _, r := range registries { - err = pkgaddon.EnableAddon(ctx, name, u.kubeClient, u.discoveryClient, u.apply, u.config, r, args.Args, u.addonRegistryCache) + err = pkgaddon.EnableAddon(ctx, name, args.Version, u.kubeClient, u.discoveryClient, u.apply, u.config, r, args.Args, u.addonRegistryCache) if err == nil { return nil } diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 5955db745..38130d300 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -77,6 +77,8 @@ func (s *addonWebService) GetWebService() *restful.WebService { Filter(s.rbacUsecase.CheckPerm("addon", "detail")). Returns(200, "OK", apis.DetailAddonResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). + Param(ws.PathParameter("name", "addon name to query detail").DataType("string").Required(true)). + Param(ws.QueryParameter("version", "specify addon version to enable").DataType("string").Required(false)). Param(ws.PathParameter("addonName", "addon name to query detail").DataType("string").Required(true)). Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Writes(apis.DetailAddonResponse{})) @@ -154,7 +156,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { name := req.PathParameter("addonName") - addon, err := s.handler.GetAddon(req.Request.Context(), name, req.QueryParameter("registry")) + addon, err := s.handler.GetAddon(req.Request.Context(), name, req.QueryParameter("registry"), req.QueryParameter("version")) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 5d65afb24..03c72ad87 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -69,6 +69,9 @@ const ( // LabelAddonName indicates the name of the corresponding Addon LabelAddonName = "addons.oam.dev/name" + // LabelAddonVersion indicates the version of the corresponding installed Addon + LabelAddonVersion = "addons.oam.dev/version" + // LabelAddonRegistry indicates the name of addon-registry LabelAddonRegistry = "addons.oam.dev/registry" diff --git a/pkg/utils/helm/helm_helper.go b/pkg/utils/helm/helm_helper.go index ff87389a2..58e2e9446 100644 --- a/pkg/utils/helm/helm_helper.go +++ b/pkg/utils/helm/helm_helper.go @@ -179,14 +179,15 @@ func (h *Helper) UninstallRelease(releaseName, namespace string, config *rest.Co // ListVersions list available versions from repo func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool) (repo.ChartVersions, error) { - i, err := h.getIndexInfo(repoURL, skipCache) + i, err := h.GetIndexInfo(repoURL, skipCache) if err != nil { return nil, err } return i.Entries[chartName], nil } -func (h *Helper) getIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile, error) { +// GetIndexInfo get index.yaml form given repo url +func (h *Helper) GetIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile, error) { if h.cache != nil && !skipCache { if i := h.cache.Get(fmt.Sprintf(repoPatten, repoURL)); i != nil { return i.(*repo.IndexFile), nil @@ -220,8 +221,6 @@ func (h *Helper) getIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile, if h.cache != nil { h.cache.Put(fmt.Sprintf(repoPatten, repoURL), i, calculateCacheTimeFromIndex(len(i.Entries))) } - - fmt.Println(len(i.Entries)) return i, nil } @@ -286,7 +285,7 @@ func newActionConfig(config *rest.Config, namespace string, showDetail bool, log // ListChartsFromRepo list available helm charts in a repo func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool) ([]string, error) { - i, err := h.getIndexInfo(repoURL, skipCache) + i, err := h.GetIndexInfo(repoURL, skipCache) if err != nil { return nil, err } @@ -306,7 +305,7 @@ func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version st return v.(map[string]interface{}), nil } } - i, err := h.getIndexInfo(repoURL, skipCache) + i, err := h.GetIndexInfo(repoURL, skipCache) if err != nil { return nil, err } diff --git a/references/cli/addon-registry.go b/references/cli/addon-registry.go index fcb1470a3..9bbde5634 100644 --- a/references/cli/addon-registry.go +++ b/references/cli/addon-registry.go @@ -38,6 +38,7 @@ const ( addonGitToken = "gitToken" addonOssType = "OSS" addonGitType = "git" + addonHelmType = "helm" ) // NewAddonRegistryCommand return an addon registry command @@ -173,7 +174,8 @@ func listAddonRegistry(ctx context.Context, c common.Args) error { table.AddRow("Name", "Type", "URL") for _, registry := range registries { var repoType, repoURL string - if registry.OSS != nil { + switch { + case registry.OSS != nil: repoType = "OSS" u, err := url.Parse(registry.OSS.Endpoint) if err != nil { @@ -187,9 +189,16 @@ func listAddonRegistry(ctx context.Context, c common.Args) error { } repoURL = fmt.Sprintf("%s://%s.%s", u.Scheme, registry.OSS.Bucket, u.Host) } - } else { + + case registry.Git != nil: repoType = "git" repoURL = fmt.Sprintf("%s/tree/master/%s", registry.Git.URL, registry.Git.Path) + case registry.Gitee != nil: + repoType = "gitee" + repoURL = fmt.Sprintf("%s/tree/master/%s", registry.Gitee.URL, registry.Gitee.Path) + case registry.Helm != nil: + repoType = "helm" + repoURL = registry.Helm.URL } table.AddRow(registry.Name, repoType, repoURL) } @@ -314,6 +323,9 @@ func getRegistryFromArgs(cmd *cobra.Command, args []string) (*pkgaddon.Registry, return nil, err } r.Git.Token = token + case addonHelmType: + r.Helm = &pkgaddon.HelmSource{} + r.Helm.URL = endpoint default: return nil, errors.New("not support addon registry type") } diff --git a/references/cli/addon.go b/references/cli/addon.go index 49dc840eb..445784d28 100644 --- a/references/cli/addon.go +++ b/references/cli/addon.go @@ -24,6 +24,8 @@ import ( "strings" "time" + "github.com/fatih/color" + "k8s.io/client-go/discovery" "helm.sh/helm/v3/pkg/strvals" @@ -69,6 +71,7 @@ const ( ) var forceDisable bool +var addonVersion string // NewAddonCommand create `addon` command func NewAddonCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { @@ -123,6 +126,8 @@ func NewAddonEnableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Com Example: `\ Enable addon by: vela addon enable +Enable addon with specify version: + vela addon enable --version Enable addon for specific clusters, (local means control plane): vela addon enable --clusters={local,cluster1,cluster2} `, @@ -165,7 +170,7 @@ Enable addon for specific clusters, (local means control plane): return fmt.Errorf("addon directory %s not found in local", addonOrDir) } - err = enableAddon(ctx, k8sClient, dc, config, name, addonArgs) + err = enableAddon(ctx, k8sClient, dc, config, name, addonVersion, addonArgs) if err != nil { return err } @@ -175,6 +180,8 @@ Enable addon for specific clusters, (local means control plane): return nil }, } + + cmd.Flags().StringVarP(&addonVersion, "version", "v", "", "specify the addon version to enable") return cmd } @@ -207,6 +214,8 @@ func NewAddonUpgradeCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Co Example: `\ Upgrade addon by: vela addon upgrade +Upgrade addon with specify version: + vela addon upgrade --version Upgrade addon for specific clusters, (local means control plane): vela addon upgrade --clusters={local,cluster1,cluster2} `, @@ -256,7 +265,7 @@ Upgrade addon for specific clusters, (local means control plane): if err != nil { return errors.Wrapf(err, "cannot fetch addon related addon %s", addonOrDir) } - err = enableAddon(ctx, k8sClient, dc, config, addonOrDir, addonArgs) + err = enableAddon(ctx, k8sClient, dc, config, addonOrDir, addonVersion, addonArgs) if err != nil { return err } @@ -267,6 +276,7 @@ Upgrade addon for specific clusters, (local means control plane): return nil }, } + cmd.Flags().StringVarP(&addonVersion, "version", "v", "", "specify the addon version to upgrade") return cmd } @@ -349,7 +359,7 @@ func NewAddonStatusCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Com } } -func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, name string, args map[string]interface{}) error { +func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, name string, version string, args map[string]interface{}) error { var err error registryDS := pkgaddon.NewRegistryDataStore(k8sClient) registries, err := registryDS.ListRegistries(ctx) @@ -358,7 +368,7 @@ func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.Dis } for _, registry := range registries { - err = pkgaddon.EnableAddon(ctx, name, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil) + err = pkgaddon.EnableAddon(ctx, name, version, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil) if errors.Is(err, pkgaddon.ErrNotExist) { continue } @@ -424,27 +434,40 @@ func listAddons(ctx context.Context, clt client.Client, registry string) error { if registry != "" && r.Name != registry { continue } - - meta, err := r.ListAddonMeta() - if err != nil { - continue + var addonList []*pkgaddon.UIData + var err error + if !pkgaddon.IsVersionRegistry(r) { + meta, err := r.ListAddonMeta() + if err != nil { + continue + } + addonList, err = r.ListUIData(meta, pkgaddon.CLIMetaOptions) + if err != nil { + continue + } + } else { + versionedRegistry := pkgaddon.BuildVersionedRegistry(r.Name, r.Helm.URL) + addonList, err = versionedRegistry.ListAddon() + if err != nil { + continue + } } - addList, err := r.ListUIData(meta, pkgaddon.CLIMetaOptions) - if err != nil { - continue - } - addons = mergeAddons(addons, addList) + addons = mergeAddons(addons, addonList) } table := uitable.New() - table.AddRow("NAME", "REGISTRY", "DESCRIPTION", "STATUS") + table.AddRow("NAME", "REGISTRY", "DESCRIPTION", "AVAILABLE-VERSIONS", "STATUS") for _, addon := range addons { status, err := pkgaddon.GetAddonStatus(ctx, clt, addon.Name) if err != nil { return err } - table.AddRow(addon.Name, addon.RegistryName, addon.Description, status.AddonPhase) + statusRow := status.AddonPhase + if len(status.InstalledVersion) != 0 { + statusRow += fmt.Sprintf(" (%s)", status.InstalledVersion) + } + table.AddRow(addon.Name, addon.RegistryName, addon.Description, genAvailableVersionInfo(addon.AvailableVersions, status), statusRow) onlineAddon[addon.Name] = true } appList := v1alpha2.ApplicationList{} @@ -492,6 +515,22 @@ func waitApplicationRunning(k8sClient client.Client, addonName string) error { } +func genAvailableVersionInfo(versions []string, status pkgaddon.Status) string { + res := "[" + for _, version := range versions { + if version == status.InstalledVersion { + col := color.New(color.Bold, color.FgGreen) + res += col.Sprintf("%s", version) + } else { + res += version + } + res += ", " + } + res = strings.TrimSuffix(res, ", ") + res += "]" + return res +} + // TransAddonName will turn addon's name from xxx/yyy to xxx-yyy func TransAddonName(name string) string { return strings.ReplaceAll(name, "/", "-")