Feat: filter definitions by which addon installed them (#4156)

* Feat: filter by source addon in `vela def list`

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Style: change header year to 2022

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Refactor: use generic filters for extensibility

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Refactor: change variable addonFilter to addonName

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: update tests according to code changes

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Refactor: unify SearchDefinition params using filters

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: simplify tests

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Style: remove redundant code

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: add tests with multiple filters

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Feat: show SOURCE-ADDON column in `def list`, if any

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Feat: add addon filter to apiserver definition-lists

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Style: fix lint issues

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Chore: update swagger doc accordingly

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: add tests for filter Applying

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Feat: add a helper function to apply filters to lists

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Style: format imports

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Feat: add OwnerAddon to DefinitionBase

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: add tests for OwnerAddon field

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: add addon util tests

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>
This commit is contained in:
Charlie Chiang
2022-06-29 10:55:50 +08:00
committed by GitHub
parent d3454ec9d5
commit 370940070b
20 changed files with 1178 additions and 168 deletions
+631 -102
View File
File diff suppressed because it is too large Load Diff
+8 -20
View File
@@ -69,6 +69,7 @@ import (
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/utils/common"
version2 "github.com/oam-dev/kubevela/version"
@@ -632,7 +633,7 @@ func formatAppFramework(addon *InstallPackage) *v1beta1.Application {
if app.Spec.Components == nil {
app.Spec.Components = []common2.ApplicationComponent{}
}
app.Name = Convert2AppName(addon.Name)
app.Name = addonutil.Addon2AppName(addon.Name)
// force override the namespace defined vela with DefaultVelaNS,this value can be modified by Env
app.SetNamespace(types.DefaultKubeVelaNS)
if app.Labels == nil {
@@ -790,7 +791,7 @@ func RenderApp(ctx context.Context, addon *InstallPackage, k8sClient client.Clie
func RenderDefinitions(addon *InstallPackage, config *rest.Config) ([]*unstructured.Unstructured, error) {
defObjs := make([]*unstructured.Unstructured, 0)
// No matter runtime mode or control mode , definition only needs to control plane k8s.
// No matter runtime mode or control mode, definition only needs to control plane k8s.
for _, def := range addon.Definitions {
obj, err := renderObject(def)
if err != nil {
@@ -1054,14 +1055,6 @@ func renderCUETemplate(elem ElementFile, parameters string, args map[string]inte
return &comp, err
}
const addonAppPrefix = "addon-"
const addonSecPrefix = "addon-secret-"
// Convert2AppName -
func Convert2AppName(name string) string {
return addonAppPrefix + name
}
// RenderArgsSecret render addon enable argument to secret
func RenderArgsSecret(addon *InstallPackage, args map[string]interface{}) *unstructured.Unstructured {
argsByte, err := json.Marshal(args)
@@ -1071,7 +1064,7 @@ func RenderArgsSecret(addon *InstallPackage, args map[string]interface{}) *unstr
sec := v1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
ObjectMeta: metav1.ObjectMeta{
Name: Convert2SecName(addon.Name),
Name: addonutil.Addon2SecName(addon.Name),
Namespace: types.DefaultKubeVelaNS,
},
Data: map[string][]byte{
@@ -1105,11 +1098,6 @@ func FetchArgsFromSecret(sec *v1.Secret) (map[string]interface{}, error) {
return res, nil
}
// Convert2SecName generate addon argument secret name
func Convert2SecName(name string) string {
return addonSecPrefix + name
}
// Installer helps addon enable, dependency-check, dispatch resources
type Installer struct {
ctx context.Context
@@ -1221,7 +1209,7 @@ func (h *Installer) installDependency(addon *InstallPackage) error {
for _, dep := range addon.Dependencies {
err := h.cli.Get(h.ctx, client.ObjectKey{
Namespace: types.DefaultKubeVelaNS,
Name: Convert2AppName(dep.Name),
Name: addonutil.Addon2AppName(dep.Name),
}, &app)
if err == nil {
continue
@@ -1250,7 +1238,7 @@ func (h *Installer) checkDependency(addon *InstallPackage) ([]string, error) {
for _, dep := range addon.Dependencies {
err := h.cli.Get(h.ctx, client.ObjectKey{
Namespace: types.DefaultKubeVelaNS,
Name: Convert2AppName(dep.Name),
Name: addonutil.Addon2AppName(dep.Name),
}, &app)
if err == nil {
continue
@@ -1405,7 +1393,7 @@ func determineAddonAppName(ctx context.Context, cli client.Client, addonName str
return "", err
}
// if the app still not exist, use addon-{addonName}
return Convert2AppName(addonName), nil
return addonutil.Addon2AppName(addonName), nil
}
return app.Name, nil
}
@@ -1414,7 +1402,7 @@ func determineAddonAppName(ctx context.Context, cli client.Client, addonName str
// if not find will try to get 1.1 legacy addon related app by using NamespacedName(vela-system, `addonName`)
func FetchAddonRelatedApp(ctx context.Context, cli client.Client, addonName string) (*v1beta1.Application, error) {
app := &v1beta1.Application{}
if err := cli.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: Convert2AppName(addonName)}, app); err != nil {
if err := cli.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2AppName(addonName)}, app); err != nil {
if !apierrors.IsNotFound(err) {
return nil, err
}
+2 -1
View File
@@ -39,6 +39,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam/util"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
@@ -252,7 +253,7 @@ var _ = Describe("Test addon util func", func() {
secArgs := v1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
ObjectMeta: metav1.ObjectMeta{
Name: Convert2SecName("test-addon-old-args"),
Name: addonutil.Addon2SecName("test-addon-old-args"),
Namespace: types.DefaultKubeVelaNS,
},
StringData: map[string]string{
+2 -1
View File
@@ -49,6 +49,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
version2 "github.com/oam-dev/kubevela/version"
)
@@ -417,7 +418,7 @@ func TestGetAddonStatus4Observability(t *testing.T) {
addonSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: Convert2SecName(ObservabilityAddon),
Name: addonutil.Addon2SecName(ObservabilityAddon),
Namespace: types.DefaultKubeVelaNS,
},
Data: map[string][]byte{},
+3 -2
View File
@@ -36,6 +36,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
@@ -162,7 +163,7 @@ func GetAddonStatus(ctx context.Context, cli client.Client, name string) (Status
// Get addon parameters
var sec v1.Secret
err = cli.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: Convert2SecName(name)}, &sec)
err = cli.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(name)}, &sec)
if err != nil {
// Not found error can be ignored. Others can't.
if !apierrors.IsNotFound(err) {
@@ -186,7 +187,7 @@ func GetAddonStatus(ctx context.Context, cli client.Client, name string) (Status
sec v1.Secret
domain string
)
if err = cli.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: Convert2SecName(name)}, &sec); err != nil {
if err = cli.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(name)}, &sec); err != nil {
klog.ErrorS(err, "failed to get observability secret")
addonStatus.AddonPhase = enabling
addonStatus.InstalledVersion = ""
+3 -2
View File
@@ -46,6 +46,7 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/apply"
velaerr "github.com/oam-dev/kubevela/pkg/utils/errors"
)
@@ -220,7 +221,7 @@ func (u *addonServiceImpl) StatusAddon(ctx context.Context, name string) (*apis.
var sec v1.Secret
err = u.kubeClient.Get(ctx, client.ObjectKey{
Namespace: types.DefaultKubeVelaNS,
Name: pkgaddon.Convert2SecName(name),
Name: addonutil.Addon2SecName(name),
}, &sec)
if err != nil && !errors2.IsNotFound(err) {
return nil, bcode.ErrAddonSecretGet
@@ -445,7 +446,7 @@ func (u *addonServiceImpl) UpdateAddon(ctx context.Context, name string, args ap
// check addon application whether exist
err := u.kubeClient.Get(ctx, client.ObjectKey{
Namespace: types.DefaultKubeVelaNS,
Name: pkgaddon.Convert2AppName(name),
Name: addonutil.Addon2AppName(name),
}, &app)
if err != nil {
return err
+24 -18
View File
@@ -21,6 +21,10 @@ import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/filters"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"
@@ -63,12 +67,13 @@ type definitionServiceImpl struct {
type DefinitionQueryOption struct {
Type string `json:"type"`
AppliedWorkloads string `json:"appliedWorkloads"`
OwnerAddon string `json:"sourceAddon"`
QueryAll bool `json:"queryAll"`
}
// String return cache key string
func (d DefinitionQueryOption) String() string {
return fmt.Sprintf("type:%s/appliedWorkloads:%s/queryAll:%v", d.Type, d.AppliedWorkloads, d.QueryAll)
return fmt.Sprintf("type:%s/appliedWorkloads:%s/ownerAddon:%s/queryAll:%v", d.Type, d.AppliedWorkloads, d.OwnerAddon, d.QueryAll)
}
const (
@@ -119,24 +124,17 @@ func (d *definitionServiceImpl) listDefinitions(ctx context.Context, list *unstr
}); err != nil {
return nil, err
}
// Apply filters to list
filteredList := filters.ApplyToList(*list,
// Filter by applied workload
filters.ByAppliedWorkload(ops.AppliedWorkloads),
// Filter by which addon installed this definition
filters.ByOwnerAddon(ops.OwnerAddon),
)
var defs []*apisv1.DefinitionBase
for _, def := range list.Items {
if ops.AppliedWorkloads != "" {
traitDef := &v1beta1.TraitDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(def.Object, traitDef); err != nil {
return nil, errors.Wrap(err, "invalid trait definition")
}
filter := false
for _, workload := range traitDef.Spec.AppliesToWorkloads {
if workload == ops.AppliedWorkloads || workload == "*" {
filter = true
break
}
}
if !filter {
continue
}
}
for _, def := range filteredList.Items {
definition, err := convertDefinitionBase(def, kind)
if err != nil {
log.Logger.Errorf("convert definition to base failure %s", err.Error())
@@ -180,6 +178,14 @@ func convertDefinitionBase(def unstructured.Unstructured, kind string) (*apisv1.
return "enable"
}(),
}
// Set OwnerAddon field
for _, ownerRef := range def.GetOwnerReferences() {
if strings.HasPrefix(ownerRef.Name, addon.AddonAppPrefix) {
definition.OwnerAddon = addon.AppName2Addon(ownerRef.Name)
// We are only interested in one owner addon
break
}
}
if kind == kindComponentDefinition {
compDef := &v1beta1.ComponentDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(def.Object, compDef); err != nil {
@@ -88,6 +88,8 @@ var _ = Describe("Test namespace service functions", func() {
// there is already a scaler trait definition in the test env
Expect(cmp.Diff(len(traits), 2)).Should(BeEmpty())
Expect(cmp.Diff(traits[0].Name, "myingress")).Should(BeEmpty())
// The OwnAddon field of myingress should not be fluxcd
Expect(traits[0].OwnerAddon).Should(Equal("fluxcd"))
Expect(traits[0].Description).ShouldNot(BeEmpty())
Expect(traits[0].Trait).ShouldNot(BeNil())
Expect(traits[0].Alias).Should(Equal("test-alias"))
@@ -145,6 +147,18 @@ var _ = Describe("Test namespace service functions", func() {
Expect(policies[0].Description).ShouldNot(BeEmpty())
Expect(policies[0].Policy.ManageHealthCheck).Should(BeTrue())
Expect(policies[0].Alias).Should(Equal("test-alias"))
By("Filtering list by owner addon")
list, err := definitionService.ListDefinitions(context.TODO(), DefinitionQueryOption{Type: "trait", OwnerAddon: "non-existent-addon"})
Expect(err).Should(Succeed())
// All results should be filtered out
Expect(list).Should(HaveLen(0))
list, err = definitionService.ListDefinitions(context.TODO(), DefinitionQueryOption{Type: "trait", OwnerAddon: "fluxcd"})
Expect(err).Should(Succeed())
// We should see myingress being kept because fluxcd is its owner
Expect(len(list) >= 1).Should(Equal(true))
Expect(list[0].Name).Should(Equal("myingress"))
})
It("Test DetailDefinition function", func() {
@@ -6,6 +6,13 @@ metadata:
definition.oam.dev/alias: test-alias
name: myingress
namespace: vela-system
ownerReferences:
- apiVersion: core.oam.dev/v1beta1
blockOwnerDeletion: true
controller: true
kind: Application
name: addon-fluxcd
uid: ed7ca39b-2420-44a1-b395-ee1820cc74c7
spec:
appliesToWorkloads:
- "*"
@@ -50,6 +50,7 @@ func (d *definitionAPIInterface) GetWebServiceRoute() *restful.WebService {
Param(ws.QueryParameter("type", "query the definition type").DataType("string").Required(true).AllowableValues(map[string]string{"component": "", "trait": "", "workflowstep": ""})).
Param(ws.QueryParameter("queryAll", "query all definitions include hidden in UI").DataType("boolean").DefaultValue("false")).
Param(ws.QueryParameter("appliedWorkload", "if specified, query the trait definition applied to the workload").DataType("string")).
Param(ws.QueryParameter("ownerAddon", "query by which addon created the definition").DataType("string")).
Returns(200, "OK", apis.ListDefinitionResponse{}).
Writes(apis.ListDefinitionResponse{}).Do(returns200, returns500))
@@ -95,6 +96,7 @@ func (d *definitionAPIInterface) listDefinitions(req *restful.Request, res *rest
definitions, err := d.DefinitionService.ListDefinitions(req.Request.Context(), service.DefinitionQueryOption{
Type: req.QueryParameter("type"),
AppliedWorkloads: req.QueryParameter("appliedWorkload"),
OwnerAddon: req.QueryParameter("ownerAddon"),
QueryAll: queryAll,
})
if err != nil {
+3 -1
View File
@@ -838,7 +838,9 @@ type DefinitionBase struct {
Labels map[string]string `json:"labels"`
// WorkloadType the component workload type
// Deprecated: it same as component.workload.type
WorkloadType string `json:"workloadType,omitempty"`
WorkloadType string `json:"workloadType,omitempty"`
// OwnerAddon indicates which addon created this definition
OwnerAddon string `json:"ownerAddon"`
Trait *v1beta1.TraitDefinitionSpec `json:"trait,omitempty"`
Component *v1beta1.ComponentDefinitionSpec `json:"component,omitempty"`
Policy *v1beta1.PolicyDefinitionSpec `json:"policy,omitempty"`
+8 -6
View File
@@ -23,6 +23,8 @@ import (
"fmt"
"strings"
"github.com/oam-dev/kubevela/pkg/utils/filters"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/format"
@@ -385,7 +387,7 @@ func ValidDefinitionTypes() []string {
}
// SearchDefinition search the Definition in k8s by traversing all possible results across types or namespaces
func SearchDefinition(definitionName string, c client.Client, definitionType string, namespace string) ([]unstructured.Unstructured, error) {
func SearchDefinition(c client.Client, definitionType, namespace string, additionalFilters ...filters.Filter) ([]unstructured.Unstructured, error) {
ctx := context.Background()
var kinds []string
if definitionType != "" {
@@ -414,11 +416,11 @@ func SearchDefinition(definitionName string, c client.Client, definitionType str
if err := c.List(ctx, &objs, listOptions...); err != nil {
return nil, errors.Wrapf(err, "failed to get %s", kind)
}
for _, obj := range objs.Items {
if definitionName == "*" || obj.GetName() == definitionName {
definitions = append(definitions, obj)
}
}
// Apply filters to the object list
filteredList := filters.ApplyToList(objs, additionalFilters...)
definitions = append(definitions, filteredList.Items...)
}
return definitions, nil
}
+31 -2
View File
@@ -22,10 +22,15 @@ import (
"strings"
"testing"
"github.com/oam-dev/kubevela/pkg/utils/filters"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
addonutils "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
@@ -42,6 +47,9 @@ func TestDefinitionBasicFunctions(t *testing.T) {
})
def.SetName("test-trait")
def.SetGVK("TraitDefinition")
def.SetOwnerReferences([]v1.OwnerReference{{
Name: addonutils.Addon2AppName("test-addon"),
}})
if _type := def.GetType(); _type != "trait" {
t.Fatalf("set gvk invalid, expected trait got %s", _type)
}
@@ -107,10 +115,31 @@ func TestDefinitionBasicFunctions(t *testing.T) {
_ = GetDefinitionDefaultSpec("WorkloadDefinition")
_ = ValidDefinitionTypes()
if _, err = SearchDefinition("*", c, "", ""); err != nil {
if _, err = SearchDefinition(c, "", ""); err != nil {
t.Fatalf("failed to search definition: %v", err)
}
if _, err = SearchDefinition("*", c, "trait", "default"); err != nil {
if _, err = SearchDefinition(c, "trait", "default"); err != nil {
t.Fatalf("failed to search definition: %v", err)
}
res, err := SearchDefinition(c, "", "", filters.ByOwnerAddon("test-addon"))
if err != nil {
t.Fatalf("failed to search definition: %v", err)
}
if len(res) < 1 {
t.Fatalf("failed to search definition with addon filter applied: %s", "no result returned")
}
res, err = SearchDefinition(c, "", "", filters.ByName("test-trait"), filters.ByOwnerAddon("test-addon"))
if err != nil {
t.Fatalf("failed to search definition: %v", err)
}
if len(res) < 1 {
t.Fatalf("failed to search definition with addon filter applied: %s", "no result returned")
}
res, err = SearchDefinition(c, "", "", filters.ByOwnerAddon("this-is-a-non-existent-addon"))
if err != nil {
t.Fatalf("failed to search definition: %v", err)
}
if len(res) >= 1 {
t.Fatalf("failed to search definition with addon filter applied: %s", "too many results returned")
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package addon
import "strings"
// AddonSecPrefix is the prefix for secret of addons
const AddonSecPrefix = "addon-secret-"
// Addon2SecName returns the secret name that contains addon arguments
func Addon2SecName(addonName string) string {
if addonName == "" {
return ""
}
return AddonSecPrefix + addonName
}
// AddonAppPrefix is the prefix for corresponding Application of an addon
const AddonAppPrefix = "addon-"
// Addon2AppName return the app name that represents the addon
func Addon2AppName(addonName string) string {
if addonName == "" {
return ""
}
return AddonAppPrefix + addonName
}
// AppName2Addon converts an addon app name to the actual addon name
// If it is not a addon app name, empty string is returned.
func AppName2Addon(appName string) string {
if !strings.HasPrefix(appName, AddonAppPrefix) {
return ""
}
return appName[len(AddonAppPrefix):]
}
+39
View File
@@ -0,0 +1,39 @@
/*
Copyright 2022 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 TestAddon2AppName(t *testing.T) {
assert.Equal(t, Addon2AppName(""), "")
assert.Equal(t, Addon2AppName("fluxcd"), AddonAppPrefix+"fluxcd")
}
func TestAddon2SecName(t *testing.T) {
assert.Equal(t, Addon2SecName(""), "")
assert.Equal(t, Addon2SecName("fluxcd"), AddonSecPrefix+"fluxcd")
}
func TestAppName2Addon(t *testing.T) {
assert.Equal(t, AppName2Addon("some"), "")
assert.Equal(t, AppName2Addon(""), "")
assert.Equal(t, AppName2Addon(AddonAppPrefix+"fluxcd"), "fluxcd")
}
+140
View File
@@ -0,0 +1,140 @@
/*
Copyright 2022 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 filters
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/utils/addon"
)
// Filter is used to filter Unstructured objects. It is basically a func(unstructured.Unstructured) bool.
type Filter func(unstructured.Unstructured) bool
// Apply applies all the provided filters to a given object.
// Then returns if the object is filtered out or not.
// Returns true if this object is kept, otherwise false.
func Apply(obj unstructured.Unstructured, filters ...Filter) bool {
// Apply all filters
for _, filter := range filters {
// If filtered out by one of the filters
if !filter(obj) {
return false
}
}
// All filters have kept this item
return true
}
// ApplyToList applies all the provided filters to a UnstructuredList.
// It only keeps items that pass all the filters.
func ApplyToList(list unstructured.UnstructuredList, filters ...Filter) unstructured.UnstructuredList {
filteredList := unstructured.UnstructuredList{Object: list.Object}
// Apply filters to each item in the list
for _, u := range list.Items {
kept := Apply(u, filters...)
if !kept {
continue
}
// Item that passed filters
filteredList.Items = append(filteredList.Items, u)
}
return filteredList
}
// KeepAll returns a filter that keeps everything
func KeepAll() Filter {
return func(unstructured.Unstructured) bool {
return true
}
}
// KeepNone returns a filter that filters out everything
func KeepNone() Filter {
return func(unstructured.Unstructured) bool {
return false
}
}
// ByOwnerAddon returns a filter that filters out what does not belong to the owner addon.
// Empty addon name will keep everything.
func ByOwnerAddon(addonName string) Filter {
if addonName == "" {
// Empty addon name, just keep everything, no further action needed
return KeepAll()
}
// Filter by which addon installed it by owner reference
// only keep the ones that belong to the addon
return func(obj unstructured.Unstructured) bool {
ownerRefs := obj.GetOwnerReferences()
isOwnedBy := false
for _, ownerRef := range ownerRefs {
if ownerRef.Name == addon.Addon2AppName(addonName) {
isOwnedBy = true
break
}
}
return isOwnedBy
}
}
// ByName returns a filter that matches the given name.
// Empty name will keep everything.
func ByName(name string) Filter {
// Keep everything
if name == "" {
return KeepAll()
}
// Filter by name
return func(obj unstructured.Unstructured) bool {
return obj.GetName() == name
}
}
// ByAppliedWorkload returns a filter that only keeps trait definitions that applies to the given workload.
// Empty workload name will keep everything.
func ByAppliedWorkload(workload string) Filter {
// Keep everything
if workload == "" {
return KeepAll()
}
return func(obj unstructured.Unstructured) bool {
traitDef := &v1beta1.TraitDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, traitDef); err != nil {
return false
}
// Search for provided workload
// If the trait definitions applies to the given workload, then it is kept.
for _, w := range traitDef.Spec.AppliesToWorkloads {
if w == workload || w == "*" {
return true
}
}
return false
}
}
+141
View File
@@ -0,0 +1,141 @@
/*
Copyright 2022 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 filters
import (
"testing"
"gotest.tools/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
)
func TestApply(t *testing.T) {
// Any of the filters rejected
assert.Equal(t, false, Apply(unstructured.Unstructured{},
KeepAll(),
KeepNone(),
))
// All filters kept
assert.Equal(t, true, Apply(unstructured.Unstructured{},
KeepAll(),
KeepAll(),
))
}
func TestApplyToList(t *testing.T) {
list := unstructured.UnstructuredList{Items: []unstructured.Unstructured{
unstructured.Unstructured{},
unstructured.Unstructured{},
}}
list.Items[0].SetName("name")
filtered := ApplyToList(list, KeepAll(), ByName("name"))
assert.Equal(t, len(filtered.Items), 1)
}
func TestKeepAll(t *testing.T) {
f := KeepAll()
assert.Equal(t, true, f(unstructured.Unstructured{}))
}
func TestKeepNone(t *testing.T) {
f := KeepNone()
assert.Equal(t, false, f(unstructured.Unstructured{}))
}
func TestByOwnerAddon(t *testing.T) {
// Test with empty addon name
f := ByOwnerAddon("")
assert.Equal(t, true, f(unstructured.Unstructured{}))
f = ByOwnerAddon("addon-name")
// Test with empty owner refs
u := unstructured.Unstructured{}
assert.Equal(t, false, f(u))
// Test with right owner refs
u.SetOwnerReferences([]v1.OwnerReference{{
Name: addonutil.Addon2AppName("addon-name"),
}})
assert.Equal(t, true, f(u))
// Test with wrong owner refs
u.SetOwnerReferences([]v1.OwnerReference{{
Name: "addon-name-2",
}})
assert.Equal(t, false, f(u))
}
func TestByName(t *testing.T) {
// Test with empty name
f := ByName("")
assert.Equal(t, true, f(unstructured.Unstructured{}))
f = ByName("name")
// Test with empty name
u := unstructured.Unstructured{}
assert.Equal(t, false, f(u))
// Test with right name
u.SetName("name")
assert.Equal(t, true, f(u))
// Test with wrong name
u.SetName("name-2")
assert.Equal(t, false, f(u))
}
func TestByAppliedWorkload(t *testing.T) {
// Test with empty workload
f := ByAppliedWorkload("")
assert.Equal(t, true, f(unstructured.Unstructured{}))
f = ByAppliedWorkload("workload")
// Test with AppliesToWorkloads=*
trait := v1beta1.TraitDefinition{}
trait.Spec.AppliesToWorkloads = []string{"*"}
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&trait)
assert.NilError(t, err)
assert.Equal(t, true, f(unstructured.Unstructured{Object: u}))
// Test with AppliesToWorkloads=workload
trait = v1beta1.TraitDefinition{}
trait.Spec.AppliesToWorkloads = []string{"workload"}
u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&trait)
assert.NilError(t, err)
assert.Equal(t, true, f(unstructured.Unstructured{Object: u}))
// Test with AppliesToWorkloads=wrong
trait = v1beta1.TraitDefinition{}
trait.Spec.AppliesToWorkloads = []string{"wrong"}
u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&trait)
assert.NilError(t, err)
assert.Equal(t, false, f(unstructured.Unstructured{Object: u}))
// Test not a definition
assert.Equal(t, false, f(unstructured.Unstructured{}))
}
+6 -5
View File
@@ -45,6 +45,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
@@ -203,7 +204,7 @@ Enable addon for specific clusters, (local means control plane):
// AdditionalEndpointPrinter will print endpoints
func AdditionalEndpointPrinter(ctx context.Context, c common.Args, k8sClient client.Client, name string, isUpgrade bool) {
fmt.Printf("Please access %s from the following endpoints:\n", name)
err := printAppEndpoints(ctx, pkgaddon.Convert2AppName(name), types.DefaultKubeVelaNS, Filter{}, c, true)
err := printAppEndpoints(ctx, addonutil.Addon2AppName(name), types.DefaultKubeVelaNS, Filter{}, c, true)
if err != nil {
fmt.Println("Get application endpoints error:", err)
return
@@ -567,8 +568,8 @@ func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c
fmt.Print(statusString)
if status.AddonPhase != statusEnabled && status.AddonPhase != statusDisabled {
fmt.Printf("diagnose addon info from application %s", pkgaddon.Convert2AppName(name))
err := printAppStatus(context.Background(), k8sClient, ioStreams, pkgaddon.Convert2AppName(name), types.DefaultKubeVelaNS, cmd, c)
fmt.Printf("diagnose addon info from application %s", addonutil.Addon2AppName(name))
err := printAppStatus(context.Background(), k8sClient, ioStreams, addonutil.Addon2AppName(name), types.DefaultKubeVelaNS, cmd, c)
if err != nil {
return err
}
@@ -869,7 +870,7 @@ func waitApplicationRunning(k8sClient client.Client, addonName string) error {
defer spinner.Stop()
for {
err := k8sClient.Get(ctx, types2.NamespacedName{Name: pkgaddon.Convert2AppName(addonName), Namespace: types.DefaultKubeVelaNS}, &app)
err := k8sClient.Get(ctx, types2.NamespacedName{Name: addonutil.Addon2AppName(addonName), Namespace: types.DefaultKubeVelaNS}, &app)
if err != nil {
return client.IgnoreNotFound(err)
}
@@ -881,7 +882,7 @@ func waitApplicationRunning(k8sClient client.Client, addonName string) error {
applySpinnerNewSuffix(spinner, fmt.Sprintf("Waiting addon application running. It is now in phase: %s (timeout %d/%d seconds)...",
phase, timeConsumed, int(timeout.Seconds())))
if timeConsumed > int(timeout.Seconds()) {
return errors.Errorf("Enabling timeout, please run \"vela status %s -n vela-system\" to check the status of the addon", pkgaddon.Convert2AppName(addonName))
return errors.Errorf("Enabling timeout, please run \"vela status %s -n vela-system\" to check the status of the addon", addonutil.Addon2AppName(addonName))
}
time.Sleep(trackInterval)
}
+42 -4
View File
@@ -50,7 +50,9 @@ import (
"github.com/oam-dev/kubevela/pkg/cue/model/sets"
"github.com/oam-dev/kubevela/pkg/cue/packages"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/filters"
"github.com/oam-dev/kubevela/references/plugins"
)
@@ -384,7 +386,7 @@ func generateTerraformTypedComponentDefinition(cmd *cobra.Command, name, kind, p
}
func getSingleDefinition(cmd *cobra.Command, definitionName string, client client.Client, definitionType string, namespace string) (*pkgdef.Definition, error) {
definitions, err := pkgdef.SearchDefinition(definitionName, client, definitionType, namespace)
definitions, err := pkgdef.SearchDefinition(client, definitionType, namespace, filters.ByName(definitionName))
if err != nil {
return nil, err
}
@@ -526,11 +528,18 @@ func NewDefinitionListCommand(c common.Args) *cobra.Command {
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
addonName, err := cmd.Flags().GetString("from")
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", "from")
}
k8sClient, err := c.GetClient()
if err != nil {
return errors.Wrapf(err, "failed to get k8s client")
}
definitions, err := pkgdef.SearchDefinition("*", k8sClient, definitionType, namespace)
definitions, err := pkgdef.SearchDefinition(k8sClient,
definitionType,
namespace,
filters.ByOwnerAddon(addonName))
if err != nil {
return err
}
@@ -538,14 +547,42 @@ func NewDefinitionListCommand(c common.Args) *cobra.Command {
cmd.Println("No definition found.")
return nil
}
// Determine if there is a definition in the list from some addons
// This is used to tell if we want the SOURCE-ADDON column
showSourceAddon := false
for _, def := range definitions {
ownerRef := def.GetOwnerReferences()
if len(ownerRef) > 0 && strings.HasPrefix(ownerRef[0].Name, addonutil.AddonAppPrefix) {
showSourceAddon = true
break
}
}
table := newUITable()
table.AddRow("NAME", "TYPE", "NAMESPACE", "DESCRIPTION")
// We only include SOURCE-ADDON if there is at least one definition from an addon
if showSourceAddon {
table.AddRow("NAME", "TYPE", "NAMESPACE", "SOURCE-ADDON", "DESCRIPTION")
} else {
table.AddRow("NAME", "TYPE", "NAMESPACE", "DESCRIPTION")
}
for _, definition := range definitions {
desc := ""
if annotations := definition.GetAnnotations(); annotations != nil {
desc = annotations[pkgdef.DescriptionKey]
}
table.AddRow(definition.GetName(), definition.GetKind(), definition.GetNamespace(), desc)
// Do not show SOURCE-ADDON column
if !showSourceAddon {
table.AddRow(definition.GetName(), definition.GetKind(), definition.GetNamespace(), desc)
continue
}
sourceAddon := ""
if len(definition.GetOwnerReferences()) > 0 {
sourceAddon = strings.TrimPrefix(definition.GetOwnerReferences()[0].Name, "addon-")
}
table.AddRow(definition.GetName(), definition.GetKind(), definition.GetNamespace(), sourceAddon, desc)
}
cmd.Println(table)
return nil
@@ -553,6 +590,7 @@ func NewDefinitionListCommand(c common.Args) *cobra.Command {
}
cmd.Flags().StringP(FlagType, "t", "", "Specify which definition type to list. If empty, all types will be searched. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", "))
cmd.Flags().StringP(Namespace, "n", "", "Specify which namespace to list. If empty, all namespaces will be searched.")
cmd.Flags().String("from", "", "Filter definitions by which addon installed them.")
return cmd
}
+19 -4
View File
@@ -36,6 +36,7 @@ import (
common3 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
)
@@ -59,12 +60,16 @@ func initCommand(cmd *cobra.Command) {
}
func createTrait(c common2.Args, t *testing.T) string {
return createTraitWithOwnerAddon(c, "", t)
}
func createTraitWithOwnerAddon(c common2.Args, addonName string, t *testing.T) string {
traitName := fmt.Sprintf("my-trait-%d", time.Now().UnixNano())
createNamespacedTrait(c, traitName, VelaTestNamespace, t)
createNamespacedTrait(c, traitName, VelaTestNamespace, addonName, t)
return traitName
}
func createNamespacedTrait(c common2.Args, name string, ns string, t *testing.T) {
func createNamespacedTrait(c common2.Args, name string, ns string, ownerAddon string, t *testing.T) {
traitName := fmt.Sprintf("my-trait-%d", time.Now().UnixNano())
client, err := c.GetClient()
if err != nil {
@@ -77,6 +82,9 @@ func createNamespacedTrait(c common2.Args, name string, ns string, t *testing.T)
Annotations: map[string]string{
pkgdef.DescriptionKey: "My test-trait " + traitName,
},
OwnerReferences: []v1.OwnerReference{{
Name: addonutil.Addon2AppName(ownerAddon),
}},
},
Spec: v1beta1.TraitDefinitionSpec{
Schematic: &common3.Schematic{CUE: &common3.CUE{Template: "parameter: {}"}},
@@ -393,7 +401,7 @@ func TestNewDefinitionGetCommand(t *testing.T) {
// test multi trait
cmd = NewDefinitionGetCommand(c)
initCommand(cmd)
createNamespacedTrait(c, traitName, "default", t)
createNamespacedTrait(c, traitName, "default", "", t)
cmd.SetArgs([]string{traitName})
if err := cmd.Execute(); err == nil {
t.Fatalf("expect found multiple traits error, but not found")
@@ -433,6 +441,13 @@ func TestNewDefinitionListCommand(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatalf("no trait found should not return error, err: %v", err)
}
// with addon filter
cmd = NewDefinitionListCommand(c)
initCommand(cmd)
cmd.SetArgs([]string{"--from", "non-existent-addon"})
if err := cmd.Execute(); err != nil {
t.Fatalf("applying addon filter should not return error, err: %v", err)
}
}
func TestNewDefinitionEditCommand(t *testing.T) {
@@ -451,7 +466,7 @@ func TestNewDefinitionEditCommand(t *testing.T) {
// test no change
cmd = NewDefinitionEditCommand(c)
initCommand(cmd)
createNamespacedTrait(c, traitName, "default", t)
createNamespacedTrait(c, traitName, "default", "", t)
if err := os.Setenv("EDITOR", "sed -i -e 's/test-trait-test/TestTrait/g'"); err != nil {
t.Fatalf("failed to set editor env: %v", err)
}