You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by li...@apache.org on 2023/05/14 03:37:56 UTC

[dubbo-admin] branch refactor-with-go updated: dubboctl: implement profile related commands and make some small fixes (#1102)

This is an automated email from the ASF dual-hosted git repository.

liujun pushed a commit to branch refactor-with-go
in repository https://gitbox.apache.org/repos/asf/dubbo-admin.git


The following commit(s) were added to refs/heads/refactor-with-go by this push:
     new 5f77f765 dubboctl: implement profile related commands and make some small fixes (#1102)
5f77f765 is described below

commit 5f77f7658fc4409e05c703e71f885e244839ff91
Author: Scout Wang <33...@users.noreply.github.com>
AuthorDate: Sun May 14 11:37:50 2023 +0800

    dubboctl: implement profile related commands and make some small fixes (#1102)
---
 go.sum                                             |   1 -
 pkg/dubboctl/cmd/manifest.go                       |  10 +-
 pkg/dubboctl/cmd/manifest_test.go                  |  90 ++++++++-----
 .../{internal/cmd/common.go => cmd/profile.go}     |  21 ++-
 pkg/dubboctl/cmd/profile_test.go                   | 143 +++++++++++++++++++++
 pkg/dubboctl/cmd/root.go                           |   1 +
 .../{internal/cmd => cmd/subcmd}/common.go         |   2 +-
 .../{internal/cmd => cmd/subcmd}/manifest_diff.go  |   2 +-
 .../cmd => cmd/subcmd}/manifest_generate.go        |  13 +-
 .../cmd => cmd/subcmd}/manifest_install.go         |   2 +-
 .../cmd => cmd/subcmd}/manifest_uninstall.go       |   2 +-
 pkg/dubboctl/cmd/subcmd/profile_diff.go            | 101 +++++++++++++++
 pkg/dubboctl/cmd/subcmd/profile_list.go            | 105 +++++++++++++++
 pkg/dubboctl/cmd/testdata/profile/test0.yaml       |  27 ++++
 pkg/dubboctl/cmd/testdata/profile/test1.yaml       |  25 ++++
 .../cmd/testdata/profile/test2_wrong_format.yaml   |  25 ++++
 pkg/dubboctl/internal/kube/client.go               |   1 -
 pkg/dubboctl/internal/kube/object.go               |   9 ++
 pkg/dubboctl/internal/kube/object_test.go          |  57 ++++++++
 pkg/dubboctl/internal/manifest/common.go           |  49 ++++++-
 .../nacos_component-render_manifest.golden.yaml    |   6 +-
 pkg/logger/log.go                                  |  11 +-
 22 files changed, 634 insertions(+), 69 deletions(-)

diff --git a/go.sum b/go.sum
index d16a2183..f68087fe 100644
--- a/go.sum
+++ b/go.sum
@@ -1167,7 +1167,6 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
diff --git a/pkg/dubboctl/cmd/manifest.go b/pkg/dubboctl/cmd/manifest.go
index d6a472b9..7bca0664 100644
--- a/pkg/dubboctl/cmd/manifest.go
+++ b/pkg/dubboctl/cmd/manifest.go
@@ -16,7 +16,7 @@
 package cmd
 
 import (
-	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/cmd"
+	subcmd "github.com/apache/dubbo-admin/pkg/dubboctl/cmd/subcmd"
 	"github.com/spf13/cobra"
 )
 
@@ -26,9 +26,9 @@ func addManifest(rootCmd *cobra.Command) {
 		Short: "Commands related to manifest",
 		Long:  "Commands help user to generate manifest and install manifest",
 	}
-	cmd.ConfigManifestGenerateCmd(manifestCmd)
-	cmd.ConfigManifestInstallCmd(manifestCmd)
-	cmd.ConfigManifestUninstallCmd(manifestCmd)
-	cmd.ConfigManifestDiffCmd(manifestCmd)
+	subcmd.ConfigManifestGenerateCmd(manifestCmd)
+	subcmd.ConfigManifestInstallCmd(manifestCmd)
+	subcmd.ConfigManifestUninstallCmd(manifestCmd)
+	subcmd.ConfigManifestDiffCmd(manifestCmd)
 	rootCmd.AddCommand(manifestCmd)
 }
diff --git a/pkg/dubboctl/cmd/manifest_test.go b/pkg/dubboctl/cmd/manifest_test.go
index e09fbdc9..0a9d941e 100644
--- a/pkg/dubboctl/cmd/manifest_test.go
+++ b/pkg/dubboctl/cmd/manifest_test.go
@@ -17,19 +17,20 @@ package cmd
 
 import (
 	"bytes"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/cmd/subcmd"
 	"os"
 	"strings"
 	"testing"
 
-	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/cmd"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
 func TestManifestGenerate(t *testing.T) {
 	tests := []struct {
-		desc string
-		cmd  string
-		temp string
+		desc    string
+		cmd     string
+		temp    string
+		wantErr bool
 	}{
 		{
 			desc: "using default configuration without any flag",
@@ -52,6 +53,14 @@ func TestManifestGenerate(t *testing.T) {
 			cmd: "manifest generate --set spec.componentsMeta.grafana.repoURL=https://grafana.github.io/helm-charts" +
 				" --set spec.componentsMeta.grafana.version=6.31.0",
 		},
+		{
+			desc: "setting specification of built-in component with wrong path",
+			cmd:  "manifest generate --set components.nacos.replicas=3",
+		},
+		{
+			desc: "setting specification of built-in component with wrong path",
+			cmd:  "manifest generate --set components.grafana.replicas=3",
+		},
 		{
 			desc: "generate manifest to target path",
 			cmd:  "manifest generate -o ./testdata/temp",
@@ -63,18 +72,21 @@ func TestManifestGenerate(t *testing.T) {
 		},
 	}
 	for _, test := range tests {
-		testExecute(t, test.cmd)
-		// remove temporary dir
-		if test.temp != "" {
-			os.RemoveAll(test.temp)
-		}
+		t.Run(test.desc, func(t *testing.T) {
+			testExecute(t, test.cmd, test.wantErr)
+			// remove temporary dir
+			if test.temp != "" {
+				os.RemoveAll(test.temp)
+			}
+		})
 	}
 }
 
 func TestManifestInstall(t *testing.T) {
 	tests := []struct {
-		desc string
-		cmd  string
+		desc    string
+		cmd     string
+		wantErr bool
 	}{
 		{
 			desc: "without any flag",
@@ -82,11 +94,13 @@ func TestManifestInstall(t *testing.T) {
 		},
 	}
 	// For now, we do not use envTest to do black box testing
-	cmd.TestInstallFlag = true
-	cmd.TestCli = fake.NewClientBuilder().Build()
+	subcmd.TestInstallFlag = true
+	subcmd.TestCli = fake.NewClientBuilder().Build()
 
 	for _, test := range tests {
-		testExecute(t, test.cmd)
+		t.Run(test.desc, func(t *testing.T) {
+			testExecute(t, test.cmd, test.wantErr)
+		})
 	}
 }
 
@@ -94,8 +108,9 @@ func TestManifestUninstall(t *testing.T) {
 	tests := []struct {
 		desc string
 		// cmd has been executed before
-		before string
-		cmd    string
+		before  string
+		cmd     string
+		wantErr bool
 	}{
 		{
 			desc:   "without any flag",
@@ -104,13 +119,15 @@ func TestManifestUninstall(t *testing.T) {
 		},
 	}
 	// For now, we do not use envTest to do black box testing
-	cmd.TestInstallFlag = true
-	cmd.TestCli = fake.NewClientBuilder().Build()
+	subcmd.TestInstallFlag = true
+	subcmd.TestCli = fake.NewClientBuilder().Build()
 
 	for _, test := range tests {
-		// prepare existing resources
-		testExecute(t, test.before)
-		testExecute(t, test.cmd)
+		t.Run(test.desc, func(t *testing.T) {
+			// prepare existing resources
+			testExecute(t, test.before, false)
+			testExecute(t, test.cmd, test.wantErr)
+		})
 	}
 }
 
@@ -120,6 +137,7 @@ func TestManifestDiff(t *testing.T) {
 		befores []string
 		cmd     string
 		temps   []string
+		wantErr bool
 	}{
 		{
 			desc: "compare two dirs",
@@ -135,25 +153,35 @@ func TestManifestDiff(t *testing.T) {
 		},
 	}
 	for _, test := range tests {
-		for _, before := range test.befores {
-			testExecute(t, before)
-		}
-		testExecute(t, test.cmd)
-		for _, temp := range test.temps {
-			if temp != "" {
-				os.RemoveAll(temp)
+		t.Run(test.desc, func(t *testing.T) {
+			for _, before := range test.befores {
+				testExecute(t, before, false)
 			}
-		}
+			testExecute(t, test.cmd, test.wantErr)
+			for _, temp := range test.temps {
+				if temp != "" {
+					os.RemoveAll(temp)
+				}
+			}
+		})
 	}
 }
 
-func testExecute(t *testing.T, cmd string) {
+func testExecute(t *testing.T, cmd string, wantErr bool) string {
 	var out bytes.Buffer
 	args := strings.Split(cmd, " ")
 	rootCmd := getRootCmd(args)
 	rootCmd.SetOut(&out)
 	if err := rootCmd.Execute(); err != nil {
+		if wantErr {
+			return ""
+		}
 		t.Errorf("execute %s failed, err: %s", cmd, err)
-		return
+		return ""
+	}
+	if wantErr {
+		t.Errorf("want err but got no err")
+		return ""
 	}
+	return out.String()
 }
diff --git a/pkg/dubboctl/internal/cmd/common.go b/pkg/dubboctl/cmd/profile.go
similarity index 66%
copy from pkg/dubboctl/internal/cmd/common.go
copy to pkg/dubboctl/cmd/profile.go
index d8de607d..e0c3534b 100644
--- a/pkg/dubboctl/internal/cmd/common.go
+++ b/pkg/dubboctl/cmd/profile.go
@@ -15,10 +15,19 @@
 
 package cmd
 
-import "sigs.k8s.io/controller-runtime/pkg/client"
-
-var (
-	// TestInstallFlag and TestCli are uses for black box testing
-	TestInstallFlag bool
-	TestCli         client.Client
+import (
+	"github.com/apache/dubbo-admin/pkg/dubboctl/cmd/subcmd"
+	"github.com/spf13/cobra"
 )
+
+func addProfile(rootCmd *cobra.Command) {
+	profileCmd := &cobra.Command{
+		Use:   "profile",
+		Short: "Commands related to profiles",
+		Long:  "Commands help user to list and describe profiles",
+	}
+	subcmd.ConfigProfileListArgs(profileCmd)
+	subcmd.ConfigProfileDiffArgs(profileCmd)
+
+	rootCmd.AddCommand(profileCmd)
+}
diff --git a/pkg/dubboctl/cmd/profile_test.go b/pkg/dubboctl/cmd/profile_test.go
new file mode 100644
index 00000000..db49d4d9
--- /dev/null
+++ b/pkg/dubboctl/cmd/profile_test.go
@@ -0,0 +1,143 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 cmd
+
+import "testing"
+
+func TestProfileList(t *testing.T) {
+	tests := []struct {
+		desc    string
+		cmd     string
+		want    string
+		wantErr bool
+	}{
+		{
+			desc: "list all profiles provided by dubbo-admin",
+			cmd:  "profile list",
+			want: `Dubbo-admin profiles:
+    default
+`,
+		},
+		{
+			desc: "list all profiles in path specified by user",
+			cmd:  "profile list --profiles ./testdata/profile",
+			want: `Dubbo-admin profiles:
+    test0
+    test1
+    test2_wrong_format
+`,
+		},
+		{
+			desc: "display selected profile",
+			cmd:  "profile list test0 --profiles ./testdata/profile",
+			want: `apiVersion: dubbo.apache.org/v1alpha1
+kind: DubboOperator
+metadata:
+  namespace: dubbo-system
+spec:
+  profile: default
+  namespace: dubbo-system
+  componentsMeta:
+    admin:
+      enabled: true
+    nacos:
+      enabled: true`,
+		},
+		{
+			desc:    "display selected profile with wrong format",
+			cmd:     "profile list test2_wrong_format --profiles ./testdata/profile",
+			wantErr: true,
+		},
+		{
+			desc:    "display selected profiles",
+			cmd:     "profile list test0 test1 --profiles ./testdata/profile",
+			wantErr: true,
+		},
+		{
+			desc: "display profile that does not exist",
+			cmd:  "profile list test2 --profiles ./testdata/profile",
+			want: "",
+		},
+		{
+			desc:    "list profile directory that does not exist",
+			cmd:     "profile list --profiles ./testdata/profile/non_exist",
+			wantErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			res := testExecute(t, test.cmd, test.wantErr)
+			if test.want != "" && test.want != res {
+				t.Errorf("want:\n%s\nbutgot:\n%s\n", test.want, res)
+				return
+			}
+		})
+	}
+}
+
+func TestProfileDiff(t *testing.T) {
+	tests := []struct {
+		desc    string
+		cmd     string
+		want    string
+		wantErr bool
+	}{
+		{
+			desc: "show the difference between two profiles provided by dubbo-admin",
+			cmd:  "profile diff default default",
+			want: `two profiles are identical
+`,
+		},
+		{
+			desc: "show the difference between two profiled specified by user",
+			cmd:  "profile diff test0 test1 --profiles ./testdata/profile",
+			want: ` apiVersion: dubbo.apache.org/v1alpha1
+ kind: DubboOperator
+ metadata:
+   namespace: dubbo-system
+ spec:
+   componentsMeta:
+     admin:
+       enabled: true
+-    nacos:
+-      enabled: true
+   namespace: dubbo-system
+   profile: default
+ `,
+		},
+		{
+			desc:    "do not specify two profiles",
+			cmd:     "profile diff test0 --profiles ./testdata/profile",
+			wantErr: true,
+		},
+		{
+			desc:    "diff profiles with wrong format",
+			cmd:     "profile diff test0 test2_wrong_format --profiles ./testdata/profile",
+			wantErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			res := testExecute(t, test.cmd, test.wantErr)
+			if test.want != "" && test.want != res {
+				t.Errorf("want:\n%s\nbutgot:\n%s\n", test.want, res)
+				return
+			}
+		})
+	}
+}
diff --git a/pkg/dubboctl/cmd/root.go b/pkg/dubboctl/cmd/root.go
index e2ea35ce..c19f8752 100644
--- a/pkg/dubboctl/cmd/root.go
+++ b/pkg/dubboctl/cmd/root.go
@@ -40,4 +40,5 @@ func getRootCmd(args []string) *cobra.Command {
 
 func addSubCommands(rootCmd *cobra.Command) {
 	addManifest(rootCmd)
+	addProfile(rootCmd)
 }
diff --git a/pkg/dubboctl/internal/cmd/common.go b/pkg/dubboctl/cmd/subcmd/common.go
similarity index 98%
rename from pkg/dubboctl/internal/cmd/common.go
rename to pkg/dubboctl/cmd/subcmd/common.go
index d8de607d..e6e72d83 100644
--- a/pkg/dubboctl/internal/cmd/common.go
+++ b/pkg/dubboctl/cmd/subcmd/common.go
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package cmd
+package subcmd
 
 import "sigs.k8s.io/controller-runtime/pkg/client"
 
diff --git a/pkg/dubboctl/internal/cmd/manifest_diff.go b/pkg/dubboctl/cmd/subcmd/manifest_diff.go
similarity index 99%
rename from pkg/dubboctl/internal/cmd/manifest_diff.go
rename to pkg/dubboctl/cmd/subcmd/manifest_diff.go
index 8743d0cc..254dde93 100644
--- a/pkg/dubboctl/internal/cmd/manifest_diff.go
+++ b/pkg/dubboctl/cmd/subcmd/manifest_diff.go
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package cmd
+package subcmd
 
 import (
 	"errors"
diff --git a/pkg/dubboctl/internal/cmd/manifest_generate.go b/pkg/dubboctl/cmd/subcmd/manifest_generate.go
similarity index 94%
rename from pkg/dubboctl/internal/cmd/manifest_generate.go
rename to pkg/dubboctl/cmd/subcmd/manifest_generate.go
index 063d783a..7904b8c5 100644
--- a/pkg/dubboctl/internal/cmd/manifest_generate.go
+++ b/pkg/dubboctl/cmd/subcmd/manifest_generate.go
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package cmd
+package subcmd
 
 import (
 	"fmt"
@@ -135,11 +135,11 @@ func addManifestGenerateFlags(cmd *cobra.Command, args *ManifestGenerateArgs) {
 func generateValues(mgArgs *ManifestGenerateArgs) (*v1alpha1.DubboConfig, string, error) {
 	mergedYaml, profile, err := manifest.ReadYamlAndProfile(mgArgs.FileNames, mgArgs.SetFlags)
 	if err != nil {
-		return nil, "", fmt.Errorf("generateValues err: %v", err)
+		return nil, "", fmt.Errorf("process user specification failed, err: %s", err)
 	}
-	profileYaml, err := manifest.ReadProfileYaml(mgArgs.ProfilesPath, profile)
+	profileYaml, err := manifest.ReadOverlayProfileYaml(mgArgs.ProfilesPath, profile)
 	if err != nil {
-		return nil, "", err
+		return nil, "", fmt.Errorf("process profile failed, err: %s", err)
 	}
 	finalYaml, err := util.OverlayYAML(profileYaml, mergedYaml)
 	if err != nil {
@@ -147,13 +147,12 @@ func generateValues(mgArgs *ManifestGenerateArgs) (*v1alpha1.DubboConfig, string
 	}
 	finalYaml, err = manifest.OverlaySetFlags(finalYaml, mgArgs.SetFlags)
 	if err != nil {
-		return nil, "", err
+		return nil, "", fmt.Errorf("process set flags failed, err: %s", err)
 	}
 	cfg := &v1alpha1.DubboConfig{}
 	if err := yaml.Unmarshal([]byte(finalYaml), cfg); err != nil {
-		return nil, "", err
+		return nil, "", fmt.Errorf("set flags specification is wrong, err: %s", err)
 	}
-	// todo: validate op
 	// we should ensure that Components field would not be nil
 	if cfg.Spec.Components == nil {
 		cfg.Spec.Components = &v1alpha1.DubboComponentsSpec{}
diff --git a/pkg/dubboctl/internal/cmd/manifest_install.go b/pkg/dubboctl/cmd/subcmd/manifest_install.go
similarity index 99%
rename from pkg/dubboctl/internal/cmd/manifest_install.go
rename to pkg/dubboctl/cmd/subcmd/manifest_install.go
index 2d3e3c6c..23aad1d4 100644
--- a/pkg/dubboctl/internal/cmd/manifest_install.go
+++ b/pkg/dubboctl/cmd/subcmd/manifest_install.go
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package cmd
+package subcmd
 
 import (
 	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/apis/dubbo.apache.org/v1alpha1"
diff --git a/pkg/dubboctl/internal/cmd/manifest_uninstall.go b/pkg/dubboctl/cmd/subcmd/manifest_uninstall.go
similarity index 99%
rename from pkg/dubboctl/internal/cmd/manifest_uninstall.go
rename to pkg/dubboctl/cmd/subcmd/manifest_uninstall.go
index 12ea9304..a84de755 100644
--- a/pkg/dubboctl/internal/cmd/manifest_uninstall.go
+++ b/pkg/dubboctl/cmd/subcmd/manifest_uninstall.go
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package cmd
+package subcmd
 
 import (
 	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/apis/dubbo.apache.org/v1alpha1"
diff --git a/pkg/dubboctl/cmd/subcmd/profile_diff.go b/pkg/dubboctl/cmd/subcmd/profile_diff.go
new file mode 100644
index 00000000..b093da9a
--- /dev/null
+++ b/pkg/dubboctl/cmd/subcmd/profile_diff.go
@@ -0,0 +1,101 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 subcmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/identifier"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/kube"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/manifest"
+	"github.com/apache/dubbo-admin/pkg/logger"
+	"github.com/spf13/cobra"
+	"go.uber.org/zap/zapcore"
+)
+
+type ProfileDiffArgs struct {
+	ProfilesPath string
+}
+
+func (pda *ProfileDiffArgs) setDefault() {
+	if pda == nil {
+		return
+	}
+	if pda.ProfilesPath == "" {
+		pda.ProfilesPath = identifier.Profiles
+	}
+}
+
+func ConfigProfileDiffArgs(baseCmd *cobra.Command) {
+	pdArgs := &ProfileDiffArgs{}
+	pdCmd := &cobra.Command{
+		Use:   "diff",
+		Short: "Show the difference between two profiles",
+		Example: `  # show the difference between two profiles provided by dubbo-admin
+  dubboctl profile diff default demo
+
+  # show the difference between two profiles specified by user
+  dubboctl profile diff profile_you_specify0 profile_you_specify1 --profiles /path/to/profiles
+`,
+		Args: func(cmd *cobra.Command, args []string) error {
+			if len(args) != 2 {
+				return errors.New("profile diff needs two profiles")
+			}
+			return nil
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			logger.InitCmdSugar(zapcore.AddSync(cmd.OutOrStdout()))
+			pdArgs.setDefault()
+			if err := profileDiffCmd(pdArgs, args); err != nil {
+				return err
+			}
+			return nil
+		},
+	}
+	pdCmd.PersistentFlags().StringVarP(&pdArgs.ProfilesPath, "profiles", "", "",
+		"Path to profiles directory, this directory contains preset profiles")
+
+	baseCmd.AddCommand(pdCmd)
+}
+
+func profileDiffCmd(pdArgs *ProfileDiffArgs, args []string) error {
+	profileA, err := manifest.ReadProfileYaml(pdArgs.ProfilesPath, args[0])
+	if err != nil {
+		return fmt.Errorf("parse %s profile failed, err: %s", args[0], err)
+	}
+	profileB, err := manifest.ReadProfileYaml(pdArgs.ProfilesPath, args[1])
+	if err != nil {
+		return fmt.Errorf("parse %s profile failed, err: %s", args[1], err)
+	}
+	objA, err := kube.ParseObjectFromManifest(profileA)
+	if err != nil {
+		return fmt.Errorf("parse %s profile to k8s object failed, err: %s", args[0], err)
+	}
+	objB, err := kube.ParseObjectFromManifest(profileB)
+	if err != nil {
+		return fmt.Errorf("parse %s profile to k8s object failed, err: %s", args[1], err)
+	}
+	diff, err := kube.CompareObject(objA, objB)
+	if err != nil {
+		return fmt.Errorf("compare failed, err: %s", err)
+	}
+	if diff != "" {
+		logger.CmdSugar().Print(diff)
+	} else {
+		logger.CmdSugar().Print("two profiles are identical\n")
+	}
+	return nil
+}
diff --git a/pkg/dubboctl/cmd/subcmd/profile_list.go b/pkg/dubboctl/cmd/subcmd/profile_list.go
new file mode 100644
index 00000000..5c0ce938
--- /dev/null
+++ b/pkg/dubboctl/cmd/subcmd/profile_list.go
@@ -0,0 +1,105 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 subcmd
+
+import (
+	"errors"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/identifier"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/manifest"
+	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/util"
+	"github.com/apache/dubbo-admin/pkg/logger"
+	"github.com/spf13/cobra"
+	"go.uber.org/zap/zapcore"
+	"strings"
+)
+
+type ProfileListArgs struct {
+	ProfilesPath string
+}
+
+func (pla *ProfileListArgs) setDefault() {
+	if pla == nil {
+		return
+	}
+	if pla.ProfilesPath == "" {
+		pla.ProfilesPath = identifier.Profiles
+	}
+}
+
+func ConfigProfileListArgs(baseCmd *cobra.Command) {
+	plArgs := &ProfileListArgs{}
+	plCmd := &cobra.Command{
+		Use:   "list",
+		Short: "List all existing profiles specification",
+		Example: `  # list all profiles provided by dubbo-admin
+  dubboctl profile list
+
+  # list all profiles in path specified by user
+  dubboctl profile list --profiles /path/to/profiles
+
+  # display selected profile
+  dubboctl profile list default
+`,
+		Args: func(cmd *cobra.Command, args []string) error {
+			if len(args) > 1 {
+				return errors.New("profile list doesn't support multi profile")
+			}
+			return nil
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			logger.InitCmdSugar(zapcore.AddSync(cmd.OutOrStdout()))
+			plArgs.setDefault()
+			if err := profileListCmd(plArgs, args); err != nil {
+				return err
+			}
+			return nil
+		},
+	}
+	plCmd.PersistentFlags().StringVarP(&plArgs.ProfilesPath, "profiles", "", "",
+		"Path to profiles directory, this directory contains preset profiles")
+
+	baseCmd.AddCommand(plCmd)
+}
+
+func profileListCmd(plArgs *ProfileListArgs, args []string) error {
+	profiles, err := manifest.ReadProfilesNames(plArgs.ProfilesPath)
+	if err != nil {
+		return err
+	}
+	// list all profiles
+	if len(args) == 0 {
+		var resBuilder strings.Builder
+		resBuilder.WriteString("Dubbo-admin profiles:\n")
+		for _, profile := range profiles {
+			resBuilder.WriteString("    " + profile + "\n")
+		}
+		logger.CmdSugar().Print(resBuilder.String())
+		return nil
+	}
+
+	for _, profile := range profiles {
+		if profile == args[0] {
+			res, err := manifest.ReadProfileYaml(plArgs.ProfilesPath, profile)
+			if err != nil {
+				return err
+			}
+			logger.CmdSugar().Print(util.ApplyFilters(res, util.LicenseFilter, util.SpaceFilter))
+			return nil
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/dubboctl/cmd/testdata/profile/test0.yaml b/pkg/dubboctl/cmd/testdata/profile/test0.yaml
new file mode 100644
index 00000000..7513bd0d
--- /dev/null
+++ b/pkg/dubboctl/cmd/testdata/profile/test0.yaml
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+apiVersion: dubbo.apache.org/v1alpha1
+kind: DubboOperator
+metadata:
+  namespace: dubbo-system
+spec:
+  profile: default
+  namespace: dubbo-system
+  componentsMeta:
+    admin:
+      enabled: true
+    nacos:
+      enabled: true
\ No newline at end of file
diff --git a/pkg/dubboctl/cmd/testdata/profile/test1.yaml b/pkg/dubboctl/cmd/testdata/profile/test1.yaml
new file mode 100644
index 00000000..030a7821
--- /dev/null
+++ b/pkg/dubboctl/cmd/testdata/profile/test1.yaml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+apiVersion: dubbo.apache.org/v1alpha1
+kind: DubboOperator
+metadata:
+  namespace: dubbo-system
+spec:
+  profile: default
+  namespace: dubbo-system
+  componentsMeta:
+    admin:
+      enabled: true
\ No newline at end of file
diff --git a/pkg/dubboctl/cmd/testdata/profile/test2_wrong_format.yaml b/pkg/dubboctl/cmd/testdata/profile/test2_wrong_format.yaml
new file mode 100644
index 00000000..7d7a52f4
--- /dev/null
+++ b/pkg/dubboctl/cmd/testdata/profile/test2_wrong_format.yaml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+apiVersion: dubbo.apache.org/v1alpha1
+kind: DubboOperator
+metadata:
+  namespace: dubbo-system
+spec:
+  profile: default
+  namespace: dubbo-system
+  componentsMeta:
+    admin:
+      enabled: right
\ No newline at end of file
diff --git a/pkg/dubboctl/internal/kube/client.go b/pkg/dubboctl/internal/kube/client.go
index 07080749..876be155 100644
--- a/pkg/dubboctl/internal/kube/client.go
+++ b/pkg/dubboctl/internal/kube/client.go
@@ -230,7 +230,6 @@ func (cli *CtlClient) deleteNamespace(ns string) error {
 		}
 		return nil
 	} else {
-		// todo: learn how to use deleteOption
 		if err := cli.Delete(context.Background(), nsObj); err != nil {
 			return fmt.Errorf("failed to delete namespace: %s, err: %s", ns, err)
 		}
diff --git a/pkg/dubboctl/internal/kube/object.go b/pkg/dubboctl/internal/kube/object.go
index 5d78b174..2f0fab11 100644
--- a/pkg/dubboctl/internal/kube/object.go
+++ b/pkg/dubboctl/internal/kube/object.go
@@ -221,3 +221,12 @@ func CompareObjects(objsA, objsB Objects) (string, string, string) {
 
 	return diffRes.String(), addRes.String(), errRes.String()
 }
+
+// CompareObject compares two objects and returns diff.
+func CompareObject(objA, objB *Object) (string, error) {
+	diff, err := util.DiffYAML(objA.YAML(), objB.YAML())
+	if err != nil {
+		return "", err
+	}
+	return diff, nil
+}
diff --git a/pkg/dubboctl/internal/kube/object_test.go b/pkg/dubboctl/internal/kube/object_test.go
index 3fa7dc93..cf255046 100644
--- a/pkg/dubboctl/internal/kube/object_test.go
+++ b/pkg/dubboctl/internal/kube/object_test.go
@@ -16,6 +16,7 @@
 package kube
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -294,3 +295,59 @@ error converting YAML to JSON: yaml: line 2: mapping values are not allowed in t
 		}
 	}
 }
+
+func TestCompareObject(t *testing.T) {
+	tests := []struct {
+		desc    string
+		objA    *Object
+		objB    *Object
+		diff    string
+		wantErr bool
+	}{
+		{
+			desc: "objects could be parsed correctly",
+			objA: &Object{
+				yamlStr: `key1: val1
+key2: val2`,
+			},
+			objB: &Object{
+				yamlStr: `key1: val1
+key2: val3`,
+			},
+			diff: `key1: val1
+-key2: val2
++key2: val3`,
+		},
+		{
+			desc: "objects with wrong format",
+			objA: &Object{
+				yamlStr: `key1: val1
+  key2: val2`,
+			},
+			objB: &Object{
+				yamlStr: `key1: val1
+key2: val3`,
+			},
+			wantErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			diff, err := CompareObject(test.objA, test.objB)
+			if err != nil {
+				if !test.wantErr {
+					t.Errorf("execute failed, err: %s", err)
+				}
+			} else {
+				if test.wantErr {
+					t.Errorf("execution expected to fail, but succeed")
+				} else {
+					if strings.TrimSpace(diff) != test.diff {
+						t.Errorf("want:\n%s\nbut got:\n%s", test.diff, diff)
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/dubboctl/internal/manifest/common.go b/pkg/dubboctl/internal/manifest/common.go
index eee109b7..c5f29cdb 100644
--- a/pkg/dubboctl/internal/manifest/common.go
+++ b/pkg/dubboctl/internal/manifest/common.go
@@ -27,12 +27,19 @@ import (
 	"sigs.k8s.io/yaml"
 )
 
-func ReadProfileYaml(profilePath string, profile string) (string, error) {
+func ReadOverlayProfileYaml(profilePath string, profile string) (string, error) {
 	filePath := path.Join(profilePath, profile+".yaml")
-	out, err := ReadAndOverlayYamls([]string{filePath})
+	defaultPath := path.Join(profilePath, "default.yaml")
+	// overlay selected profile ont default profile
+	out, err := ReadAndOverlayYamls([]string{defaultPath, filePath})
 	if err != nil {
 		return "", err
 	}
+	// unmarshal and validate
+	tempOp := &v1alpha1.DubboConfig{}
+	if err := yaml.Unmarshal([]byte(out), tempOp); err != nil {
+		return "", fmt.Errorf("%s profile on default profile is wrong, err: %s", profile, err)
+	}
 	return out, nil
 }
 
@@ -44,7 +51,7 @@ func ReadYamlAndProfile(filenames []string, setFlags []string) (string, string,
 	// unmarshal and validate
 	tempOp := &v1alpha1.DubboConfig{}
 	if err := yaml.Unmarshal([]byte(mergedYaml), tempOp); err != nil {
-		return "", "", fmt.Errorf("ReadYamlAndProfile failed, err: %s", err)
+		return "", "", fmt.Errorf("user specification is wrong, err: %s", err)
 	}
 	// get profile field and overlay with setFlags
 	profile := "default"
@@ -95,3 +102,39 @@ func OverlaySetFlags(base string, setFlags []string) (string, error) {
 	}
 	return string(finalYaml), nil
 }
+
+// ReadProfilesNames reads all profiles in directory specified by profilesPath.
+// It does not traverse recursively.
+// It may add some filters in the future.
+func ReadProfilesNames(profilesPath string) ([]string, error) {
+	var res []string
+	dir, err := os.ReadDir(profilesPath)
+	if err != nil {
+		return nil, err
+	}
+	yamlSuffix := ".yaml"
+	for _, file := range dir {
+		if file.IsDir() {
+			continue
+		}
+		if strings.HasSuffix(file.Name(), yamlSuffix) {
+			res = append(res, strings.TrimSuffix(file.Name(), ".yaml"))
+		}
+	}
+	return res, nil
+}
+
+// ReadProfileYaml reads profile yaml specified by profilePath/profile.yaml and validates the content.
+func ReadProfileYaml(profilePath string, profile string) (string, error) {
+	filePath := path.Join(profilePath, profile+".yaml")
+	out, err := ReadAndOverlayYamls([]string{filePath})
+	if err != nil {
+		return "", err
+	}
+	// unmarshal and validate
+	tempOp := &v1alpha1.DubboConfig{}
+	if err := yaml.Unmarshal([]byte(out), tempOp); err != nil {
+		return "", fmt.Errorf("%s profile is wrong, err: %s", profile, err)
+	}
+	return out, nil
+}
diff --git a/pkg/dubboctl/internal/operator/testdata/want/nacos_component-render_manifest.golden.yaml b/pkg/dubboctl/internal/operator/testdata/want/nacos_component-render_manifest.golden.yaml
index 01a04703..a272fbb0 100644
--- a/pkg/dubboctl/internal/operator/testdata/want/nacos_component-render_manifest.golden.yaml
+++ b/pkg/dubboctl/internal/operator/testdata/want/nacos_component-render_manifest.golden.yaml
@@ -62,7 +62,9 @@ spec:
               name: raft-rpc
             - containerPort: 7848
               name: old-raft-rpc
-          resources: null
+          resources:
+            limits: {}
+            requests: {}
           startupProbe:
             httpGet:
               path: /nacos/v1/console/health/readiness
@@ -84,7 +86,6 @@ spec:
       volumes:
         - emptyDir: {}
           name: data
-
 ---
 apiVersion: v1
 kind: Service
@@ -116,5 +117,4 @@ spec:
   selector:
     app.kubernetes.io/name: nacos
   type: NodePort
-
 ---
diff --git a/pkg/logger/log.go b/pkg/logger/log.go
index 197a4e21..928e7907 100644
--- a/pkg/logger/log.go
+++ b/pkg/logger/log.go
@@ -25,10 +25,9 @@ import (
 )
 
 var (
-	mutex      = &sync.Mutex{}
-	hasInit    = false
-	cmdHasInit = false
-	encoder    = zapcore.NewConsoleEncoder(
+	mutex   = &sync.Mutex{}
+	hasInit = false
+	encoder = zapcore.NewConsoleEncoder(
 		zapcore.EncoderConfig{
 			MessageKey:     "msg",
 			LevelKey:       "level",
@@ -80,10 +79,6 @@ func Init() {
 func InitCmdSugar(ws zapcore.WriteSyncer) {
 	mutex.Lock()
 	defer mutex.Unlock()
-	if cmdHasInit {
-		return
-	}
-	cmdHasInit = true
 
 	core := zapcore.NewCore(encoder, ws, zap.DebugLevel)
 	cmdLogger = zap.New(core)