From 370940070bbf592623e588361652b911d2fff5f2 Mon Sep 17 00:00:00 2001 From: Charlie Chiang Date: Wed, 29 Jun 2022 10:55:50 +0800 Subject: [PATCH] Feat: filter definitions by which addon installed them (#4156) * Feat: filter by source addon in `vela def list` Signed-off-by: Charlie Chiang * Style: change header year to 2022 Signed-off-by: Charlie Chiang * Refactor: use generic filters for extensibility Signed-off-by: Charlie Chiang * Refactor: change variable addonFilter to addonName Signed-off-by: Charlie Chiang * Test: update tests according to code changes Signed-off-by: Charlie Chiang * Refactor: unify SearchDefinition params using filters Signed-off-by: Charlie Chiang * Test: simplify tests Signed-off-by: Charlie Chiang * Style: remove redundant code Signed-off-by: Charlie Chiang * Test: add tests with multiple filters Signed-off-by: Charlie Chiang * Feat: show SOURCE-ADDON column in `def list`, if any Signed-off-by: Charlie Chiang * Feat: add addon filter to apiserver definition-lists Signed-off-by: Charlie Chiang * Style: fix lint issues Signed-off-by: Charlie Chiang * Chore: update swagger doc accordingly Signed-off-by: Charlie Chiang * Test: add tests for filter Applying Signed-off-by: Charlie Chiang * Feat: add a helper function to apply filters to lists Signed-off-by: Charlie Chiang * Style: format imports Signed-off-by: Charlie Chiang * Feat: add OwnerAddon to DefinitionBase Signed-off-by: Charlie Chiang * Test: add tests for OwnerAddon field Signed-off-by: Charlie Chiang * Test: add addon util tests Signed-off-by: Charlie Chiang --- docs/apidoc/swagger.json | 733 +++++++++++++++--- pkg/addon/addon.go | 28 +- pkg/addon/addon_suite_test.go | 3 +- pkg/addon/addon_test.go | 3 +- pkg/addon/helper.go | 5 +- pkg/apiserver/domain/service/addon.go | 5 +- pkg/apiserver/domain/service/definition.go | 42 +- .../domain/service/definition_test.go | 14 + .../domain/service/testdata/myingress-td.yaml | 7 + pkg/apiserver/interfaces/api/definition.go | 2 + pkg/apiserver/interfaces/api/dto/v1/types.go | 4 +- pkg/definition/definition.go | 14 +- pkg/definition/definition_test.go | 33 +- pkg/utils/addon/addon.go | 53 ++ pkg/utils/addon/addon_test.go | 39 + pkg/utils/filters/filter.go | 140 ++++ pkg/utils/filters/filter_test.go | 141 ++++ references/cli/addon.go | 11 +- references/cli/def.go | 46 +- references/cli/def_test.go | 23 +- 20 files changed, 1178 insertions(+), 168 deletions(-) create mode 100644 pkg/utils/addon/addon.go create mode 100644 pkg/utils/addon/addon_test.go create mode 100644 pkg/utils/filters/filter.go create mode 100644 pkg/utils/filters/filter_test.go diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 7cdbb3d3c..891b77fa4 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -693,6 +693,14 @@ "summary": "compare application with env latest revision", "operationId": "compareAppWithLatestRevision", "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.AppCompareReq" + } + }, { "type": "string", "description": "identifier of the application ", @@ -705,7 +713,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.ApplicationBase" + "$ref": "#/definitions/v1.AppCompareResponse" } }, "400": { @@ -1193,6 +1201,14 @@ "summary": "dry-run application to latest revision", "operationId": "dryRunAppOrRevision", "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.AppDryRunReq" + } + }, { "type": "string", "description": "identifier of the application ", @@ -3655,6 +3671,12 @@ "description": "if specified, query the trait definition applied to the workload", "name": "appliedWorkload", "in": "query" + }, + { + "type": "string", + "description": "query by which addon created the definition", + "name": "ownerAddon", + "in": "query" } ], "responses": { @@ -4790,55 +4812,6 @@ } } }, - "/api/v1/projects/{projectName}/validate_image": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "project" - ], - "summary": "validate an image in a project", - "operationId": "validateImage", - "parameters": [ - { - "type": "string", - "description": "image name", - "name": "image", - "in": "query" - }, - { - "type": "string", - "description": "identifier of the project", - "name": "projectName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/*v1.ImageResponse" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, "/api/v1/query": { "get": { "consumes": [ @@ -4899,7 +4872,8 @@ "type": "string", "description": "the config project", "name": "project", - "in": "query" + "in": "query", + "required": true } ], "responses": { @@ -5062,6 +5036,101 @@ } } }, + "/api/v1/repository/image/info": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "repository", + "helm" + ], + "summary": "get the oci repos", + "operationId": "getImageInfo", + "parameters": [ + { + "type": "string", + "description": "the config project", + "name": "project", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "the image name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "the secret name of the image repository", + "name": "secretName", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ImageInfo" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/repository/image/repos": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "repository", + "helm" + ], + "summary": "get the oci repos", + "operationId": "getImageRepos", + "parameters": [ + { + "type": "string", + "description": "the config project", + "name": "project", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListImageRegistryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/roles": { "get": { "consumes": [ @@ -5894,7 +5963,6 @@ "*v1.ApplicationTriggerBase": {}, "*v1.Config": {}, "*v1.EmptyResponse": {}, - "*v1.ImageResponse": {}, "addon.Dependency": { "properties": { "name": { @@ -6720,6 +6788,13 @@ } } }, + "common.WorkflowStepMeta": { + "properties": { + "alias": { + "type": "string" + } + } + }, "common.WorkflowStepStatus": { "required": [ "id" @@ -6769,12 +6844,18 @@ "type": "string" } }, + "if": { + "type": "string" + }, "inputs": { "type": "array", "items": { "$ref": "#/definitions/common.inputItem" } }, + "meta": { + "$ref": "#/definitions/common.WorkflowStepMeta" + }, "name": { "type": "string" }, @@ -6787,6 +6868,9 @@ "properties": { "type": "string" }, + "timeout": { + "type": "string" + }, "type": { "type": "string" } @@ -7117,8 +7201,8 @@ }, "model.Cluster": { "required": [ - "updateTime", "createTime", + "updateTime", "name", "alias", "description", @@ -7669,8 +7753,8 @@ }, "v1.AddonStatusResponse": { "required": [ - "phase", "name", + "phase", "args" ], "properties": { @@ -7709,6 +7793,16 @@ "v1.AddonStatusResponse.clusters": { "type": "object" }, + "v1.AppCompareReq": { + "required": [ + "env" + ], + "properties": { + "env": { + "type": "string" + } + } + }, "v1.AppCompareResponse": { "required": [ "isDiff", @@ -7731,6 +7825,28 @@ } } }, + "v1.AppDryRunReq": { + "required": [ + "appName", + "dryRunType", + "env", + "version" + ], + "properties": { + "appName": { + "type": "string" + }, + "dryRunType": { + "type": "string" + }, + "env": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "v1.AppDryRunResponse": { "required": [ "yaml" @@ -7826,11 +7942,11 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "status", + "version", "note", "triggerType", "createTime", - "version", + "status", "envName" ], "properties": { @@ -8368,6 +8484,154 @@ } } }, + "v1.Config": { + "properties": { + "ArgsEscaped": { + "type": "boolean" + }, + "AttachStderr": { + "type": "boolean" + }, + "AttachStdin": { + "type": "boolean" + }, + "AttachStdout": { + "type": "boolean" + }, + "Cmd": { + "type": "array", + "items": { + "type": "string" + } + }, + "Domainname": { + "type": "string" + }, + "Entrypoint": { + "type": "array", + "items": { + "type": "string" + } + }, + "Env": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExposedPorts": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/v1.Config.ExposedPorts" + } + }, + "Healthcheck": { + "$ref": "#/definitions/v1.HealthConfig" + }, + "Hostname": { + "type": "string" + }, + "Image": { + "type": "string" + }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "MacAddress": { + "type": "string" + }, + "NetworkDisabled": { + "type": "boolean" + }, + "OnBuild": { + "type": "array", + "items": { + "type": "string" + } + }, + "OpenStdin": { + "type": "boolean" + }, + "Shell": { + "type": "array", + "items": { + "type": "string" + } + }, + "StdinOnce": { + "type": "boolean" + }, + "StopSignal": { + "type": "string" + }, + "Tty": { + "type": "boolean" + }, + "User": { + "type": "string" + }, + "Volumes": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/v1.Config.Volumes" + } + }, + "WorkingDir": { + "type": "string" + } + } + }, + "v1.Config.ExposedPorts": {}, + "v1.Config.Volumes": {}, + "v1.ConfigFile": { + "required": [ + "architecture", + "os", + "rootfs", + "config" + ], + "properties": { + "architecture": { + "type": "string" + }, + "author": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/v1.Config" + }, + "container": { + "type": "string" + }, + "created": { + "type": "string" + }, + "docker_version": { + "type": "string" + }, + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.History" + } + }, + "os": { + "type": "string" + }, + "os.version": { + "type": "string" + }, + "rootfs": { + "$ref": "#/definitions/v1.RootFS" + }, + "variant": { + "type": "string" + } + } + }, "v1.ConfigType": { "required": [ "definitions", @@ -9026,13 +9290,50 @@ } } }, + "v1.Descriptor": { + "required": [ + "mediaType", + "size", + "digest" + ], + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "data": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "mediaType": { + "type": "string" + }, + "platform": { + "$ref": "#/definitions/v1.Platform" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "urls": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.DetailAddonResponse": { "required": [ - "invisible", - "name", "description", + "invisible", "version", "icon", + "name", "schema", "uiSchema", "definitions", @@ -9112,13 +9413,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "name", - "project", - "description", "icon", "alias", + "project", + "description", "createTime", "updateTime", + "name", "policies", "envBindings", "resourceInfo" @@ -9175,20 +9476,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "name", - "description", "createTime", - "icon", - "status", - "reason", - "provider", - "apiServerURL", - "alias", - "dashboardURL", "updateTime", + "reason", + "dashboardURL", "labels", + "apiServerURL", "kubeConfig", "kubeConfigSecret", + "name", + "alias", + "icon", + "status", + "description", + "provider", "resourceInfo" ], "properties": { @@ -9246,14 +9547,14 @@ }, "v1.DetailComponentResponse": { "required": [ - "creator", - "appPrimaryKey", - "alias", "type", - "main", "createTime", - "updateTime", "name", + "updateTime", + "alias", + "creator", + "main", + "appPrimaryKey", "definition" ], "properties": { @@ -9341,11 +9642,11 @@ }, "v1.DetailDefinitionResponse": { "required": [ - "alias", - "description", + "name", "status", "labels", - "name", + "alias", + "description", "icon", "schema", "uiSchema" @@ -9400,14 +9701,14 @@ }, "v1.DetailPolicyResponse": { "required": [ - "createTime", "updateTime", "envName", "name", "type", "description", "creator", - "properties" + "properties", + "createTime" ], "properties": { "createTime": { @@ -9440,17 +9741,17 @@ }, "v1.DetailRevisionResponse": { "required": [ - "createTime", - "updateTime", - "reason", - "appPrimaryKey", "version", - "status", "envName", + "createTime", + "appPrimaryKey", + "reason", "deployUser", - "note", + "workflowName", + "updateTime", "triggerType", - "workflowName" + "status", + "note" ], "properties": { "appPrimaryKey": { @@ -9504,9 +9805,9 @@ }, "v1.DetailTargetResponse": { "required": [ + "project", "createTime", "updateTime", - "project", "name" ], "properties": { @@ -9547,11 +9848,11 @@ }, "v1.DetailUserResponse": { "required": [ - "disabled", "createTime", "lastLoginTime", "name", "email", + "disabled", "projects", "roles" ], @@ -9592,12 +9893,12 @@ }, "v1.DetailWorkflowRecordResponse": { "required": [ - "applicationRevision", - "status", - "name", "namespace", "workflowName", "workflowAlias", + "applicationRevision", + "status", + "name", "deployTime", "deployUser", "note", @@ -9649,14 +9950,14 @@ }, "v1.DetailWorkflowResponse": { "required": [ + "createTime", + "updateTime", + "name", + "description", + "envName", "alias", "enable", - "createTime", - "name", - "default", - "envName", - "updateTime", - "description" + "default" ], "properties": { "alias": { @@ -9851,8 +10152,8 @@ }, "v1.EnvBindingTarget": { "required": [ - "alias", - "name" + "name", + "alias" ], "properties": { "alias": { @@ -9889,6 +10190,119 @@ } } }, + "v1.Hash": { + "required": [ + "Algorithm", + "Hex" + ], + "properties": { + "Algorithm": { + "type": "string" + }, + "Hex": { + "type": "string" + } + } + }, + "v1.HealthConfig": { + "properties": { + "Interval": { + "type": "integer", + "format": "integer" + }, + "Retries": { + "type": "integer", + "format": "int32" + }, + "StartPeriod": { + "type": "integer", + "format": "integer" + }, + "Test": { + "type": "array", + "items": { + "type": "string" + } + }, + "Timeout": { + "type": "integer", + "format": "integer" + } + } + }, + "v1.History": { + "properties": { + "author": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "created": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "empty_layer": { + "type": "boolean" + } + } + }, + "v1.ImageInfo": { + "required": [ + "name", + "secretNames", + "registry", + "size", + "manifest" + ], + "properties": { + "info": { + "$ref": "#/definitions/v1.ConfigFile" + }, + "manifest": { + "$ref": "#/definitions/v1.Manifest" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "secretNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "size": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ImageRegistry": { + "required": [ + "name", + "secretName", + "domain" + ], + "properties": { + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secretName": { + "type": "string" + } + } + }, "v1.ListAddonRegistryResponse": { "required": [ "registries" @@ -10063,6 +10477,19 @@ } } }, + "v1.ListImageRegistryResponse": { + "required": [ + "registries" + ], + "properties": { + "registries": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ImageRegistry" + } + } + } + }, "v1.ListProjectResponse": { "required": [ "projects", @@ -10235,11 +10662,11 @@ }, "v1.LoginUserInfoResponse": { "required": [ + "email", + "disabled", "createTime", "lastLoginTime", "name", - "email", - "disabled", "projects", "platformPermissions", "projectPermissions" @@ -10288,6 +10715,37 @@ } } }, + "v1.Manifest": { + "required": [ + "schemaVersion", + "config", + "layers" + ], + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "config": { + "$ref": "#/definitions/v1.Descriptor" + }, + "layers": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Descriptor" + } + }, + "mediaType": { + "type": "string" + }, + "schemaVersion": { + "type": "integer", + "format": "int64" + } + } + }, "v1.NameAlias": { "required": [ "name", @@ -10377,6 +10835,38 @@ } } }, + "v1.Platform": { + "required": [ + "architecture", + "os" + ], + "properties": { + "architecture": { + "type": "string" + }, + "features": { + "type": "array", + "items": { + "type": "string" + } + }, + "os": { + "type": "string" + }, + "os.features": { + "type": "array", + "items": { + "type": "string" + } + }, + "os.version": { + "type": "string" + }, + "variant": { + "type": "string" + } + } + }, "v1.PolicyBase": { "required": [ "name", @@ -10520,6 +11010,23 @@ } } }, + "v1.RootFS": { + "required": [ + "type", + "diff_ids" + ], + "properties": { + "diff_ids": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Hash" + } + }, + "type": { + "type": "string" + } + } + }, "v1.SimpleResponse": { "required": [ "status" @@ -11203,6 +11710,9 @@ }, "v1beta1.Workflow": { "properties": { + "mode": { + "$ref": "#/definitions/v1beta1.WorkflowExecuteMode" + }, "ref": { "type": "string" }, @@ -11214,6 +11724,16 @@ } } }, + "v1beta1.WorkflowExecuteMode": { + "properties": { + "steps": { + "type": "string" + }, + "subSteps": { + "type": "string" + } + } + }, "v1beta1.WorkflowStep": { "required": [ "name", @@ -11226,12 +11746,18 @@ "type": "string" } }, + "if": { + "type": "string" + }, "inputs": { "type": "array", "items": { "$ref": "#/definitions/common.inputItem" } }, + "meta": { + "$ref": "#/definitions/common.WorkflowStepMeta" + }, "name": { "type": "string" }, @@ -11250,6 +11776,9 @@ "$ref": "#/definitions/common.WorkflowSubStep" } }, + "timeout": { + "type": "string" + }, "type": { "type": "string" } diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 2011f09ab..5c385d228 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -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 } diff --git a/pkg/addon/addon_suite_test.go b/pkg/addon/addon_suite_test.go index 532c41dab..b4965ba16 100644 --- a/pkg/addon/addon_suite_test.go +++ b/pkg/addon/addon_suite_test.go @@ -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{ diff --git a/pkg/addon/addon_test.go b/pkg/addon/addon_test.go index 694ac0246..6b6888d82 100644 --- a/pkg/addon/addon_test.go +++ b/pkg/addon/addon_test.go @@ -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{}, diff --git a/pkg/addon/helper.go b/pkg/addon/helper.go index d76141d01..0eca75e6f 100644 --- a/pkg/addon/helper.go +++ b/pkg/addon/helper.go @@ -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 = "" diff --git a/pkg/apiserver/domain/service/addon.go b/pkg/apiserver/domain/service/addon.go index a6c669a44..8aedcf178 100644 --- a/pkg/apiserver/domain/service/addon.go +++ b/pkg/apiserver/domain/service/addon.go @@ -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 diff --git a/pkg/apiserver/domain/service/definition.go b/pkg/apiserver/domain/service/definition.go index 5475e2786..0e29a6358 100644 --- a/pkg/apiserver/domain/service/definition.go +++ b/pkg/apiserver/domain/service/definition.go @@ -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 { diff --git a/pkg/apiserver/domain/service/definition_test.go b/pkg/apiserver/domain/service/definition_test.go index 684519cd0..79cef40aa 100644 --- a/pkg/apiserver/domain/service/definition_test.go +++ b/pkg/apiserver/domain/service/definition_test.go @@ -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() { diff --git a/pkg/apiserver/domain/service/testdata/myingress-td.yaml b/pkg/apiserver/domain/service/testdata/myingress-td.yaml index 9523235cf..e0e6ab36a 100644 --- a/pkg/apiserver/domain/service/testdata/myingress-td.yaml +++ b/pkg/apiserver/domain/service/testdata/myingress-td.yaml @@ -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: - "*" diff --git a/pkg/apiserver/interfaces/api/definition.go b/pkg/apiserver/interfaces/api/definition.go index a95181fef..b3b2954f8 100644 --- a/pkg/apiserver/interfaces/api/definition.go +++ b/pkg/apiserver/interfaces/api/definition.go @@ -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 { diff --git a/pkg/apiserver/interfaces/api/dto/v1/types.go b/pkg/apiserver/interfaces/api/dto/v1/types.go index 6740bd25b..b945d967e 100644 --- a/pkg/apiserver/interfaces/api/dto/v1/types.go +++ b/pkg/apiserver/interfaces/api/dto/v1/types.go @@ -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"` diff --git a/pkg/definition/definition.go b/pkg/definition/definition.go index 6946d75de..15a04f9dc 100644 --- a/pkg/definition/definition.go +++ b/pkg/definition/definition.go @@ -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 } diff --git a/pkg/definition/definition_test.go b/pkg/definition/definition_test.go index 8d37441d1..974263167 100644 --- a/pkg/definition/definition_test.go +++ b/pkg/definition/definition_test.go @@ -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") + } } diff --git a/pkg/utils/addon/addon.go b/pkg/utils/addon/addon.go new file mode 100644 index 000000000..42a261c4b --- /dev/null +++ b/pkg/utils/addon/addon.go @@ -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):] +} diff --git a/pkg/utils/addon/addon_test.go b/pkg/utils/addon/addon_test.go new file mode 100644 index 000000000..25c10f53b --- /dev/null +++ b/pkg/utils/addon/addon_test.go @@ -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") +} diff --git a/pkg/utils/filters/filter.go b/pkg/utils/filters/filter.go new file mode 100644 index 000000000..62415457e --- /dev/null +++ b/pkg/utils/filters/filter.go @@ -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 + } +} diff --git a/pkg/utils/filters/filter_test.go b/pkg/utils/filters/filter_test.go new file mode 100644 index 000000000..42b615f9a --- /dev/null +++ b/pkg/utils/filters/filter_test.go @@ -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{})) +} diff --git a/references/cli/addon.go b/references/cli/addon.go index aff749fb4..9cd2c5607 100644 --- a/references/cli/addon.go +++ b/references/cli/addon.go @@ -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) } diff --git a/references/cli/def.go b/references/cli/def.go index 21a911361..32b6a93bf 100644 --- a/references/cli/def.go +++ b/references/cli/def.go @@ -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 } diff --git a/references/cli/def_test.go b/references/cli/def_test.go index bc18031fb..ec1db3a84 100644 --- a/references/cli/def_test.go +++ b/references/cli/def_test.go @@ -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) }