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 000000000..640c145be Binary files /dev/null and b/pkg/addon/testdata/helm-repo/fluxcd-1.0.0.tgz differ 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, "/", "-")