diff --git a/build/test/integration_test.go b/build/test/integration_test.go index 498a8a31e2..fb566bfe4f 100644 --- a/build/test/integration_test.go +++ b/build/test/integration_test.go @@ -577,6 +577,7 @@ func TestCreateHTTPProxy(t *testing.T) { assert.Empty(stderr) defer func() { deleteResource("httpserver", "http-proxy-test") + deleteResource("pipeline", "--all") }() output, err := getResource("httpserver") @@ -1312,3 +1313,228 @@ filters: // get this body means our pipeline is working. assert.Equal("body from response builder", string(data)) } + +func TestEgctlNamespace(t *testing.T) { + assert := assert.New(t) + mockNamespace := ` +name: mockNamespace +kind: MockNamespacer +namespace: mockNamespace +httpservers: +- kind: HTTPServer + name: mock-httpserver + port: 10222 + https: false + rules: + - paths: + - path: /pipeline + backend: mock-pipeline +pipelines: +- name: mock-pipeline + kind: Pipeline + flow: + - filter: proxy + filters: + - name: proxy + kind: Proxy + pools: + - servers: + - url: http://127.0.0.1:9095 + - url: http://127.0.0.1:9096 + loadBalance: + policy: roundRobin +` + err := createResource(mockNamespace) + assert.Nil(err) + defer func() { + err := deleteResource("MockNamespacer", "mockNamespace") + assert.Nil(err) + }() + + httpserver := ` +name: httpserver-test +kind: HTTPServer +port: 10181 +https: false +keepAlive: true +keepAliveTimeout: 75s +maxConnection: 10240 +cacheSize: 0 +rules: + - paths: + - backend: pipeline-test +` + err = createResource(httpserver) + assert.Nil(err) + defer func() { + err := deleteResource("HTTPServer", "httpserver-test") + assert.Nil(err) + }() + + pipeline := ` +name: pipeline-test +kind: Pipeline +flow: +- filter: proxy +filters: +- name: proxy + kind: Proxy + pools: + - servers: + - url: http://127.0.0.1:8888 +` + err = createResource(pipeline) + assert.Nil(err) + defer func() { + err := deleteResource("Pipeline", "pipeline-test") + assert.Nil(err) + }() + + // egctl get all + { + // by default, list resources in "default" namespace + cmd := egctlCmd("get", "all") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.Contains(output, "httpserver-test") + assert.Contains(output, "pipeline-test") + assert.NotContains(output, "mock-httpserver") + assert.NotContains(output, "mock-pipeline") + } + + // egctl get all --all-namespaces + { + cmd := egctlCmd("get", "all", "--all-namespaces") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.Contains(output, "httpserver-test") + assert.Contains(output, "pipeline-test") + assert.Contains(output, "mock-httpserver") + assert.Contains(output, "mock-pipeline") + } + + // egctl get all --namespace mockNamespace + { + cmd := egctlCmd("get", "all", "--namespace", "mockNamespace") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.NotContains(output, "httpserver-test") + assert.NotContains(output, "pipeline-test") + assert.Contains(output, "mock-httpserver") + assert.Contains(output, "mock-pipeline") + } + + // egctl get all --namespace default + { + cmd := egctlCmd("get", "all", "--namespace", "default") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.Contains(output, "httpserver-test") + assert.Contains(output, "pipeline-test") + assert.NotContains(output, "mock-httpserver") + assert.NotContains(output, "mock-pipeline") + } + + // egctl get hs --namespace mockNamespace + { + cmd := egctlCmd("get", "hs", "--namespace", "mockNamespace") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.NotContains(output, "httpserver-test") + assert.NotContains(output, "pipeline-test") + assert.Contains(output, "mock-httpserver") + assert.NotContains(output, "mock-pipeline") + } + + // egctl get hs --all-namespaces + { + cmd := egctlCmd("get", "hs", "--all-namespaces") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.Contains(output, "httpserver-test") + assert.NotContains(output, "pipeline-test") + assert.Contains(output, "mock-httpserver") + assert.NotContains(output, "mock-pipeline") + } + + // egctl get hs mock-httpserver --namespace mockNamespace -o yaml + { + cmd := egctlCmd("get", "hs", "mock-httpserver", "--namespace", "mockNamespace", "-o", "yaml") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.NotContains(output, "httpserver-test") + assert.NotContains(output, "pipeline-test") + assert.Contains(output, "mock-httpserver") + assert.Contains(output, "port: 10222") + } + + // easegress update status every 5 seconds + time.Sleep(5 * time.Second) + + // egctl describe httpserver --all-namespaces + { + cmd := egctlCmd("describe", "httpserver", "--all-namespaces") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.Contains(output, "Name: httpserver-test") + assert.Contains(output, "Name: mock-httpserver") + assert.NotContains(output, "Name: pipeline-test") + assert.NotContains(output, "Name: mock-pipeline") + assert.Contains(output, "In Namespace default") + assert.Contains(output, "In Namespace mockNamespace") + // check if status is updated + assert.Equal(2, strings.Count(output, "node: primary-single")) + assert.Equal(2, strings.Count(output, "m1ErrPercent: 0")) + } + + // egctl describe httpserver --namespace mockNamespace + { + cmd := egctlCmd("describe", "httpserver", "--namespace", "mockNamespace") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.Contains(output, "Name: mock-httpserver") + assert.NotContains(output, "Name: httpserver-test") + assert.NotContains(output, "Name: pipeline-test") + assert.NotContains(output, "Name: mock-pipeline") + // check if status is updated + assert.Equal(1, strings.Count(output, "node: primary-single")) + assert.Equal(1, strings.Count(output, "m1ErrPercent: 0")) + } + + // egctl describe pipeline --namespace default + { + cmd := egctlCmd("describe", "pipeline", "--namespace", "default") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.NotContains(output, "Name: httpserver-test") + assert.Contains(output, "Name: pipeline-test") + assert.NotContains(output, "Name: mock-httpserver") + assert.NotContains(output, "Name: mock-pipeline") + // check if status is updated + assert.Equal(1, strings.Count(output, "node: primary-single")) + assert.Equal(1, strings.Count(output, "p999: 0")) + } + + // egctl describe hs mock-httpserver --namespace mockNamespace -o yaml + { + cmd := egctlCmd("describe", "hs", "mock-httpserver", "--namespace", "mockNamespace", "-o", "yaml") + output, stderr, err := runCmd(cmd) + assert.Nil(err) + assert.Empty(stderr) + assert.NotContains(output, "httpserver-test") + assert.NotContains(output, "pipeline-test") + assert.Contains(output, "name: mock-httpserver") + assert.Contains(output, "port: 10222") + assert.Contains(output, "node: primary-single") + } +} diff --git a/cmd/client/commandv2/describe.go b/cmd/client/commandv2/describe.go index 5833f94952..28594c2842 100644 --- a/cmd/client/commandv2/describe.go +++ b/cmd/client/commandv2/describe.go @@ -26,6 +26,8 @@ import ( "github.com/spf13/cobra" ) +var describeFlags resources.ObjectNamespaceFlags + // DescribeCmd returns describe command. func DescribeCmd() *cobra.Command { examples := []general.Example{ @@ -37,6 +39,8 @@ func DescribeCmd() *cobra.Command { {Desc: "Describe all members", Command: "egctl describe member"}, {Desc: "Describe a customdata kind", Command: "egctl describe customdatakind "}, {Desc: "Describe a customdata of given kind", Command: "egctl describe customdata "}, + {Desc: "Describe pipeline resources from all namespaces, including httpservers and pipelines created by IngressController, MeshController and GatewayController", Command: "egctl describe pipeline --all-namespaces"}, + {Desc: "Describe httpserver resources from a certain namespace", Command: "egctl describe httpserver --namespace "}, {Desc: "Check all possible api resources", Command: "egctl api-resources"}, } cmd := &cobra.Command{ @@ -47,6 +51,11 @@ func DescribeCmd() *cobra.Command { Run: describeCmdRun, } cmd.Flags().BoolVarP(&general.CmdGlobalFlags.Verbose, "verbose", "v", false, "Print verbose information") + cmd.Flags().StringVar(&describeFlags.Namespace, "namespace", "", + "namespace is used to describe httpservers and pipelines created by IngressController, MeshController or GatewayController"+ + "(these objects create httpservers and pipelines in an independent namespace)") + cmd.Flags().BoolVar(&describeFlags.AllNamespace, "all-namespaces", false, + "describe all resources in all namespaces (including the ones created by IngressController, MeshController and GatewayController that are in an independent namespace)") return cmd } @@ -71,7 +80,7 @@ func describeCmdRun(cmd *cobra.Command, args []string) { case resources.Member().Kind: err = resources.DescribeMember(cmd, a) default: - err = resources.DescribeObject(cmd, a, kind) + err = resources.DescribeObject(cmd, a, kind, &describeFlags) } } diff --git a/cmd/client/commandv2/get.go b/cmd/client/commandv2/get.go index 76dfcba21e..dae91a1f6e 100644 --- a/cmd/client/commandv2/get.go +++ b/cmd/client/commandv2/get.go @@ -28,6 +28,8 @@ import ( "github.com/spf13/cobra" ) +var getFlags resources.ObjectNamespaceFlags + // GetCmd returns get command. func GetCmd() *cobra.Command { examples := []general.Example{ @@ -41,6 +43,8 @@ func GetCmd() *cobra.Command { {Desc: "Get a customdata kind", Command: "egctl get customdatakind "}, {Desc: "Get a customdata of given kind", Command: "egctl get customdata "}, {Desc: "Check all possible api resources", Command: "egctl api-resources"}, + {Desc: "Check all possible api resources from all namespaces, including httpservers and pipelines created by IngressController, MeshController and GatewayController", Command: "egctl get all --all-namespaces"}, + {Desc: "Check all possible api resources from certain namespace", Command: "egctl get all --namespace "}, } cmd := &cobra.Command{ Use: "get", @@ -49,6 +53,11 @@ func GetCmd() *cobra.Command { Example: createMultiExample(examples), Run: getCmdRun, } + cmd.Flags().StringVar(&getFlags.Namespace, "namespace", "", + "namespace is used to get httpservers and pipelines created by IngressController, MeshController or GatewayController"+ + "(these objects create httpservers and pipelines in an independent namespace)") + cmd.Flags().BoolVar(&getFlags.AllNamespace, "all-namespaces", false, + "get all resources in all namespaces (including the ones created by IngressController, MeshController and GatewayController that are in an independent namespace)") return cmd } @@ -59,13 +68,18 @@ func getAllResources(cmd *cobra.Command) error { errs = append(errs, err.Error()) } } - err := resources.GetAllObject(cmd) + err := resources.GetAllObject(cmd, &getFlags) if err != nil { appendErr(err) } else { fmt.Printf("\n") } + // non-default namespace is not supported for members and custom data kinds. + if getFlags.Namespace != "" && getFlags.Namespace != resources.DefaultNamespace { + return nil + } + funcs := []func(*cobra.Command, *general.ArgInfo) error{ resources.GetMember, resources.GetCustomDataKind, } @@ -109,7 +123,7 @@ func getCmdRun(cmd *cobra.Command, args []string) { case resources.Member().Kind: err = resources.GetMember(cmd, a) default: - err = resources.GetObject(cmd, a, kind) + err = resources.GetObject(cmd, a, kind, &getFlags) } } diff --git a/cmd/client/resources/object.go b/cmd/client/resources/object.go index 104d24c8a9..4c0ed28327 100644 --- a/cmd/client/resources/object.go +++ b/cmd/client/resources/object.go @@ -22,8 +22,10 @@ import ( "errors" "fmt" "net/http" + "net/url" "os" "sort" + "strconv" "strings" "time" @@ -35,8 +37,19 @@ import ( "github.com/spf13/cobra" ) +// ObjectNamespaceFlags contains the flags for get objects. +type ObjectNamespaceFlags struct { + Namespace string + AllNamespace bool +} + +var globalAPIResources []*api.APIResource + // ObjectAPIResources returns the object api resources. func ObjectAPIResources() ([]*api.APIResource, error) { + if globalAPIResources != nil { + return globalAPIResources, nil + } url := makePath(general.ObjectAPIResources) body, err := handleReq(http.MethodGet, url, nil) if err != nil { @@ -47,30 +60,41 @@ func ObjectAPIResources() ([]*api.APIResource, error) { if err != nil { return nil, err } + globalAPIResources = res return res, nil } -func defaultObjectNameSpace() string { - return cluster.TrafficNamespace(cluster.NamespaceDefault) +// trafficObjectStatusNamespace return namespace of traffic object status. +// In easegress, when we put traffic object like httpserver, pipeline into namespace "default", +// then their status will be put into namespace "eg-traffic-default". +// the status is updated by TrafficController. +func trafficObjectStatusNamespace(objectNamespace string) string { + return cluster.TrafficNamespace(objectNamespace) } -func httpGetObject(name string) ([]byte, error) { - url := func(name string) string { +func httpGetObject(name string, flags *ObjectNamespaceFlags) ([]byte, error) { + objectURL := func(name string) string { if len(name) == 0 { return makePath(general.ObjectsURL) } return makePath(general.ObjectItemURL, name) }(name) - return handleReq(http.MethodGet, url, nil) + if flags != nil { + values := url.Values{} + values.Add("namespace", flags.Namespace) + values.Add("all-namespaces", strconv.FormatBool(flags.AllNamespace)) + objectURL = fmt.Sprintf("%s?%s", objectURL, values.Encode()) + } + return handleReq(http.MethodGet, objectURL, nil) } // GetAllObject gets all objects. -func GetAllObject(cmd *cobra.Command) error { +func GetAllObject(cmd *cobra.Command, flags *ObjectNamespaceFlags) error { getErr := func(err error) error { return general.ErrorMsg(general.GetCmd, err, "resource") } - body, err := httpGetObject("") + body, err := httpGetObject("", flags) if err != nil { return getErr(err) } @@ -80,14 +104,18 @@ func GetAllObject(cmd *cobra.Command) error { return nil } - metas, err := unmarshalMetaSpec(body, true) + if flags.AllNamespace { + err := unmarshalPrintNamespaceMetaSpec(body, nil) + if err != nil { + return getErr(err) + } + return nil + } + + err = unmarshalPrintMetaSpec(body, true, nil) if err != nil { return getErr(err) } - sort.Slice(metas, func(i, j int) bool { - return metas[i].Name < metas[j].Name - }) - printMetaSpec(metas) return nil } @@ -125,7 +153,7 @@ func EditObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { } func getObjectYaml(objectName string) (string, error) { - body, err := httpGetObject(objectName) + body, err := httpGetObject(objectName, nil) if err != nil { return "", err } @@ -154,7 +182,7 @@ func getObjectYaml(objectName string) (string, error) { } // GetObject gets an object. -func GetObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { +func GetObject(cmd *cobra.Command, args *general.ArgInfo, kind string, flags *ObjectNamespaceFlags) error { msg := fmt.Sprintf("all %s", kind) if args.ContainName() { msg = fmt.Sprintf("%s %s", kind, args.Name) @@ -163,7 +191,7 @@ func GetObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { return general.ErrorMsg(general.GetCmd, err, msg) } - body, err := httpGetObject(args.Name) + body, err := httpGetObject(args.Name, flags) if err != nil { return getErr(err) } @@ -173,7 +201,6 @@ func GetObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { general.PrintBody(body) return nil } - maps, err := general.UnmarshalMapInterface(body, true) if err != nil { return getErr(err) @@ -189,14 +216,33 @@ func GetObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { return nil } - metas, err := unmarshalMetaSpec(body, !args.ContainName()) - if err != nil { - return getErr(err) + if flags.AllNamespace { + err := unmarshalPrintNamespaceMetaSpec(body, func(m *supervisor.MetaSpec) bool { + return m.Kind == kind + }) + if err != nil { + return getErr(err) + } + return nil } - metas = general.Filter(metas, func(m *supervisor.MetaSpec) bool { + + err = unmarshalPrintMetaSpec(body, !args.ContainName(), func(m *supervisor.MetaSpec) bool { return m.Kind == kind }) + if err != nil { + return getErr(err) + } + return nil +} +func unmarshalPrintMetaSpec(body []byte, list bool, filter func(*supervisor.MetaSpec) bool) error { + metas, err := unmarshalMetaSpec(body, list) + if err != nil { + return err + } + if filter != nil { + metas = general.Filter(metas, filter) + } sort.Slice(metas, func(i, j int) bool { return metas[i].Name < metas[j].Name }) @@ -204,6 +250,28 @@ func GetObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { return nil } +func unmarshalPrintNamespaceMetaSpec(body []byte, filter func(*supervisor.MetaSpec) bool) error { + allMetas, err := unmarshalNamespaceMetaSpec(body) + if err != nil { + return err + } + if filter != nil { + for k, v := range allMetas { + allMetas[k] = general.Filter(v, filter) + } + } + for k, v := range allMetas { + if len(v) > 0 { + sort.Slice(v, func(i, j int) bool { + return v[i].Name < v[j].Name + }) + allMetas[k] = v + } + } + printNamespaceMetaSpec(allMetas) + return nil +} + func unmarshalMetaSpec(body []byte, listBody bool) ([]*supervisor.MetaSpec, error) { if listBody { metas := []*supervisor.MetaSpec{} @@ -215,6 +283,12 @@ func unmarshalMetaSpec(body []byte, listBody bool) ([]*supervisor.MetaSpec, erro return []*supervisor.MetaSpec{meta}, err } +func unmarshalNamespaceMetaSpec(body []byte) (map[string][]*supervisor.MetaSpec, error) { + res := map[string][]*supervisor.MetaSpec{} + err := codectool.Unmarshal(body, &res) + return res, err +} + func getAgeFromMetaSpec(meta *supervisor.MetaSpec) string { createdAt, err := time.Parse(time.RFC3339, meta.CreatedAt) if err != nil { @@ -223,6 +297,27 @@ func getAgeFromMetaSpec(meta *supervisor.MetaSpec) string { return general.DurationMostSignificantUnit(time.Since(createdAt)) } +func printNamespaceMetaSpec(metas map[string][]*supervisor.MetaSpec) { + // Output: + // NAME KIND NAMESPACE AGE + // ... + table := [][]string{} + table = append(table, []string{"NAME", "KIND", "NAMESPACE", "AGE"}) + defaults := metas[DefaultNamespace] + for _, meta := range defaults { + table = append(table, []string{meta.Name, meta.Kind, DefaultNamespace, getAgeFromMetaSpec(meta)}) + } + for namespace, metas := range metas { + if namespace == DefaultNamespace { + continue + } + for _, meta := range metas { + table = append(table, []string{meta.Name, meta.Kind, namespace, getAgeFromMetaSpec(meta)}) + } + } + general.PrintTable(table) +} + func printMetaSpec(metas []*supervisor.MetaSpec) { // Output: // NAME KIND AGE @@ -236,7 +331,7 @@ func printMetaSpec(metas []*supervisor.MetaSpec) { } // DescribeObject describes an object. -func DescribeObject(cmd *cobra.Command, args *general.ArgInfo, kind string) error { +func DescribeObject(cmd *cobra.Command, args *general.ArgInfo, kind string, flags *ObjectNamespaceFlags) error { msg := fmt.Sprintf("all %s", kind) if args.ContainName() { msg = fmt.Sprintf("%s %s", kind, args.Name) @@ -245,27 +340,50 @@ func DescribeObject(cmd *cobra.Command, args *general.ArgInfo, kind string) erro return general.ErrorMsg(general.GetCmd, err, msg) } - body, err := httpGetObject(args.Name) + body, err := httpGetObject(args.Name, flags) if err != nil { return getErr(err) } - specs, err := general.UnmarshalMapInterface(body, !args.ContainName()) - if err != nil { - return getErr(err) + namespaceSpecs := map[string][]map[string]interface{}{} + if flags.AllNamespace { + err := codectool.Unmarshal(body, &namespaceSpecs) + if err != nil { + return getErr(err) + } + } else { + specs, err := general.UnmarshalMapInterface(body, !args.ContainName()) + if err != nil { + return getErr(err) + } + namespace := DefaultNamespace + if flags.Namespace != "" { + namespace = flags.Namespace + } + namespaceSpecs[namespace] = specs } - specs = general.Filter(specs, func(m map[string]interface{}) bool { - return m["kind"] == kind - }) + for namespace, specs := range namespaceSpecs { + namespaceSpecs[namespace] = general.Filter(specs, func(m map[string]interface{}) bool { + return m["kind"] == kind + }) + } - err = addObjectStatusToSpec(specs, args) + err = addObjectStatusToSpec(namespaceSpecs, args, flags) if err != nil { return getErr(err) } if !general.CmdGlobalFlags.DefaultFormat() { - body, err = codectool.MarshalJSON(specs) + var input interface{} + if flags.AllNamespace { + input = namespaceSpecs + } else { + for _, v := range namespaceSpecs { + input = v + } + } + body, err = codectool.MarshalJSON(input) if err != nil { return getErr(err) } @@ -299,10 +417,39 @@ func DescribeObject(cmd *cobra.Command, args *general.ArgInfo, kind string) erro // status: ... // - node: eg2 // status: ... - general.PrintMapInterface(specs, specials, []string{objectStatusKeyInSpec}) + + printSpace := false + specs := namespaceSpecs[DefaultNamespace] + if len(specs) > 0 { + printNamespace(DefaultNamespace) + general.PrintMapInterface(specs, specials, []string{objectStatusKeyInSpec}) + printSpace = true + } + for k, v := range namespaceSpecs { + if k == DefaultNamespace { + continue + } + if len(v) == 0 { + continue + } + if printSpace { + fmt.Println() + fmt.Println() + } + printNamespace(k) + general.PrintMapInterface(v, specials, []string{objectStatusKeyInSpec}) + printSpace = true + } return nil } +func printNamespace(ns string) { + msg := fmt.Sprintf("In Namespace %s:", ns) + fmt.Println(strings.Repeat("=", len(msg))) + fmt.Println(msg) + fmt.Println(strings.Repeat("=", len(msg))) +} + // CreateObject creates an object. func CreateObject(cmd *cobra.Command, s *general.Spec) error { _, err := handleReq(http.MethodPost, makePath(general.ObjectsURL), []byte(s.Doc())) @@ -325,7 +472,7 @@ func DeleteObject(cmd *cobra.Command, kind string, names []string, all bool) err } // get all objects and filter by kind - body, err := httpGetObject("") + body, err := httpGetObject("", nil) if err != nil { return getErr(err) } @@ -368,7 +515,7 @@ func DeleteObject(cmd *cobra.Command, kind string, names []string, all bool) err // ApplyObject applies an object. func ApplyObject(cmd *cobra.Command, s *general.Spec) error { checkObjExist := func(cmd *cobra.Command, name string) bool { - _, err := httpGetObject(name) + _, err := httpGetObject(name, nil) return err == nil } @@ -414,7 +561,7 @@ func splitObjectStatusKey(key string) (*objectStatusInfo, error) { }, nil } -// ObjectStatus is the status of an object. +// ObjectStatus is the status of an TrafficObject. type ObjectStatus struct { Spec map[string]interface{} `json:"spec"` Status map[string]interface{} `json:"status"` @@ -423,6 +570,13 @@ type ObjectStatus struct { func unmarshalObjectStatus(data []byte) (ObjectStatus, error) { var status ObjectStatus err := codectool.Unmarshal(data, &status) + // if status.Spec and status.Status are all nil + // then means the status is not traffic controller object status (not httpserver, pipeline). + // we need to re-unmarshal to get true status. + if status.Spec == nil && status.Status == nil { + status.Status = map[string]interface{}{} + codectool.Unmarshal(data, &status.Status) + } return status, err } @@ -439,10 +593,6 @@ func unmarshalObjectStatusInfo(body []byte, name string) ([]*objectStatusInfo, e if err != nil { return nil, err } - // only show objects in default namespace, objects in other namespaces is not created by user. - if info.namespace != defaultObjectNameSpace() { - continue - } if name != "" && info.name != name { continue } @@ -464,18 +614,20 @@ func unmarshalObjectStatusInfo(body []byte, name string) ([]*objectStatusInfo, e // NodeStatus is the status of a node. type NodeStatus struct { - Node string - Status map[string]interface{} + Node string `json:"node"` + Status map[string]interface{} `json:"status"` } const objectStatusKeyInSpec = "allStatus" -func addObjectStatusToSpec(specs []map[string]interface{}, args *general.ArgInfo) error { +func addObjectStatusToSpec(allSpecs map[string][]map[string]interface{}, args *general.ArgInfo, flags *ObjectNamespaceFlags) error { getUrl := func(args *general.ArgInfo) string { - if !args.ContainName() { + // no name, all namespaces, non default namespace, then get all status + if !args.ContainName() || flags.AllNamespace { return makePath(general.StatusObjectsURL) } - return makePath(general.StatusObjectItemURL, args.Name) + url := makePath(general.StatusObjectItemURL, args.Name) + return fmt.Sprintf("%s?namespace=%s", url, flags.Namespace) } body, err := handleReq(http.MethodGet, getUrl(args), nil) @@ -488,18 +640,52 @@ func addObjectStatusToSpec(specs []map[string]interface{}, args *general.ArgInfo return err } + keyFn := func(namespace, name string) string { + return namespace + "/" + name + } // key is name, value is array of node and status status := map[string][]*NodeStatus{} for _, info := range infos { - status[info.name] = append(status[info.name], &NodeStatus{ + key := keyFn(info.namespace, info.name) + status[key] = append(status[key], &NodeStatus{ Node: info.node, Status: info.status, }) } - for _, s := range specs { - name := s["name"].(string) - s[objectStatusKeyInSpec] = status[name] + categoryMap := func() map[string]string { + rs, err := ObjectAPIResources() + if err != nil { + return map[string]string{} + } + res := map[string]string{} + for _, r := range rs { + res[r.Kind] = r.Category + } + return res + }() + getNamespace := func(kind string, namespace string) string { + category := categoryMap[kind] + if category == "" { + switch kind { + case "HTTPServer": + category = supervisor.CategoryTrafficGate + case "Pipeline": + category = supervisor.CategoryPipeline + } + } + if category == supervisor.CategoryPipeline || category == supervisor.CategoryTrafficGate { + return trafficObjectStatusNamespace(namespace) + } + return namespace + } + for namespace, specs := range allSpecs { + for i := range specs { + spec := specs[i] + ns := getNamespace(spec["kind"].(string), namespace) + name := spec["name"].(string) + allSpecs[namespace][i][objectStatusKeyInSpec] = status[keyFn(ns, name)] + } } return nil } diff --git a/cmd/client/resources/resources.go b/cmd/client/resources/resources.go index 91ff005004..02318cad49 100644 --- a/cmd/client/resources/resources.go +++ b/cmd/client/resources/resources.go @@ -28,6 +28,8 @@ import ( "github.com/megaease/easegress/v2/cmd/client/general" ) +const DefaultNamespace = "default" + // GetResourceKind returns the kind of the resource. func GetResourceKind(arg string) (string, error) { if general.InAPIResource(arg, CustomData()) { diff --git a/docs/02.Tutorials/2.1.egctl-Usage.md b/docs/02.Tutorials/2.1.egctl-Usage.md index b03294cde9..bf929bea1d 100644 --- a/docs/02.Tutorials/2.1.egctl-Usage.md +++ b/docs/02.Tutorials/2.1.egctl-Usage.md @@ -144,6 +144,17 @@ egctl describe httpserver # describe all HTTPServer resource egctl describe pipeline pipeline-demo # describe Pipeline resource with name "pipeline-demo" ``` +In Easegress, when using `IngressController`, `MeshController`, or `GatewayController`, resources such as `HTTPServers` and `Pipelines` are created in separate namespaces to prevent conflicts. To access these resources, you can utilize the `--namespace` or `--all-namespaces` argument. + +> The use of these arguments is relevant only when employing `IngressController`, `MeshController`, or `GatewayController`. Easegress does not support creating resources in different namespaces by using apis or `egctl` command. + +```bash +egctl get all --all-namespaces # view all resources in all namespace. +egctl get all --namespace default # view all resources in default namespace. +egctl describe pipeline --all-namespaces # describe pipeline resources in all namespace. +egctl describe httpserver demo --namespace default # describe httpserver demo in default namespace. +``` + ## Updating resources ```bash egctl apply -f httpserver-demo-version2.yaml # update HTTPServer resource diff --git a/pkg/api/cluster.go b/pkg/api/cluster.go index 5525fd1bb6..071b0d1301 100644 --- a/pkg/api/cluster.go +++ b/pkg/api/cluster.go @@ -116,8 +116,19 @@ func (s *Server) _deleteObject(name string) { } } -func (s *Server) _getStatusObject(name string) map[string]interface{} { - prefix := s.cluster.Layout().StatusObjectPrefix(cluster.TrafficNamespace(cluster.NamespaceDefault), name) +// _getStatusObject returns the status object with the specified name. +// in easegress, since TrafficController contain multiply namespaces. +// and it use special prefix to store status of httpserver, pipeline, or grpcserver. +// so we need to diff them by using isTraffic. previous, we actually can't get status for business controller like autocertmanager. +func (s *Server) _getStatusObject(namespace string, name string, isTraffic bool) map[string]interface{} { + ns := namespace + if ns == "" { + ns = cluster.NamespaceDefault + } + prefix := s.cluster.Layout().StatusObjectPrefix(ns, name) + if isTraffic { + prefix = s.cluster.Layout().StatusObjectPrefix(cluster.TrafficNamespace(ns), name) + } kvs, err := s.cluster.GetPrefix(prefix) if err != nil { ClusterPanic(err) diff --git a/pkg/api/object.go b/pkg/api/object.go index 09763fb4f9..ce02a3da01 100644 --- a/pkg/api/object.go +++ b/pkg/api/object.go @@ -22,10 +22,12 @@ import ( "io" "net/http" "sort" + "strconv" "strings" "github.com/go-chi/chi/v5" + "github.com/megaease/easegress/v2/pkg/object/trafficcontroller" "github.com/megaease/easegress/v2/pkg/supervisor" "github.com/megaease/easegress/v2/pkg/util/codectool" ) @@ -270,15 +272,24 @@ func (s *Server) getObjectTemplate(w http.ResponseWriter, r *http.Request) { func (s *Server) getObject(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") + _, namespace := parseNamespaces(r) + if namespace != "" && namespace != DefaultNamespace { + spec := s._getObjectByNamespace(namespace, name) + if spec == nil { + HandleAPIError(w, r, http.StatusNotFound, fmt.Errorf("not found")) + return + } - // No need to lock. + WriteBody(w, r, spec) + return + } + // No need to lock. spec := s._getObject(name) if spec == nil { HandleAPIError(w, r, http.StatusNotFound, fmt.Errorf("not found")) return } - WriteBody(w, r, spec) } @@ -320,7 +331,35 @@ func (s *Server) updateObject(w http.ResponseWriter, r *http.Request) { s.upgradeConfigVersion(w, r) } +func parseNamespaces(r *http.Request) (bool, string) { + allNamespaces := strings.TrimSpace(r.URL.Query().Get("all-namespaces")) + namespace := strings.TrimSpace(r.URL.Query().Get("namespace")) + flag, err := strconv.ParseBool(allNamespaces) + if err != nil { + return false, namespace + } + return flag, namespace +} + func (s *Server) listObjects(w http.ResponseWriter, r *http.Request) { + allNamespaces, namespace := parseNamespaces(r) + if allNamespaces && namespace != "" { + HandleAPIError(w, r, http.StatusBadRequest, fmt.Errorf("conflict query params, can't set all-namespaces and namespace at the same time")) + return + } + if allNamespaces { + allSpecs := s._listAllNamespaces() + allSpecs[DefaultNamespace] = s._listObjects() + WriteBody(w, r, allSpecs) + return + } + if namespace != "" && namespace != DefaultNamespace { + specs := s._listNamespaces(namespace) + WriteBody(w, r, specs) + return + } + + // allNamespaces == false && namespace == "" // No need to lock. specs := specList(s._listObjects()) // NOTE: Keep it consistent. @@ -331,16 +370,21 @@ func (s *Server) listObjects(w http.ResponseWriter, r *http.Request) { func (s *Server) getStatusObject(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") + _, namespace := parseNamespaces(r) - spec := s._getObject(name) - + var spec *supervisor.Spec + if namespace == "" || namespace == DefaultNamespace { + spec = s._getObject(name) + } else { + spec = s._getObjectByNamespace(namespace, name) + } if spec == nil { HandleAPIError(w, r, http.StatusNotFound, fmt.Errorf("not found")) return } - status := s._getStatusObject(name) - + _, isTraffic := supervisor.TrafficObjectKinds[spec.Kind()] + status := s._getStatusObject(namespace, name, isTraffic) WriteBody(w, r, status) } @@ -387,3 +431,66 @@ func (s *Server) listObjectAPIResources(w http.ResponseWriter, r *http.Request) res := ObjectAPIResources() WriteBody(w, r, res) } + +func getTrafficController(super *supervisor.Supervisor) *trafficcontroller.TrafficController { + entity, exists := super.GetSystemController(trafficcontroller.Kind) + if !exists { + return nil + } + tc, ok := entity.Instance().(*trafficcontroller.TrafficController) + if !ok { + return nil + } + return tc +} + +func (s *Server) _listAllNamespaces() map[string][]*supervisor.Spec { + tc := getTrafficController(s.super) + if tc == nil { + return nil + } + res := make(map[string][]*supervisor.Spec) + allObjects := tc.ListAllNamespace() + for namespace, objects := range allObjects { + specs := make([]*supervisor.Spec, 0, len(objects)) + for _, o := range objects { + specs = append(specs, o.Spec()) + } + res[namespace] = specs + } + return res +} + +func (s *Server) _listNamespaces(ns string) []*supervisor.Spec { + tc := getTrafficController(s.super) + if tc == nil { + return nil + } + traffics := tc.ListTrafficGates(ns) + pipelines := tc.ListPipelines(ns) + specs := make([]*supervisor.Spec, 0, len(traffics)+len(pipelines)) + for _, t := range traffics { + specs = append(specs, t.Spec()) + } + for _, p := range pipelines { + specs = append(specs, p.Spec()) + } + return specs +} + +func (s *Server) _getObjectByNamespace(ns string, name string) *supervisor.Spec { + tc := getTrafficController(s.super) + if tc == nil { + return nil + } + object, ok := tc.GetPipeline(ns, name) + if ok { + return object.Spec() + } + + object, ok = tc.GetTrafficGate(ns, name) + if ok { + return object.Spec() + } + return nil +} diff --git a/pkg/api/registry.go b/pkg/api/registry.go index 98898882b7..472a4ebc95 100644 --- a/pkg/api/registry.go +++ b/pkg/api/registry.go @@ -20,10 +20,24 @@ package api import ( "fmt" + "strings" + "github.com/megaease/easegress/v2/pkg/object/trafficcontroller" "github.com/megaease/easegress/v2/pkg/supervisor" ) +func init() { + // to avoid import cycle + RegisterObject(&APIResource{ + Category: trafficcontroller.Category, + Kind: trafficcontroller.Kind, + Name: strings.ToLower(trafficcontroller.Kind), + Aliases: []string{"trafficcontroller", "tc"}, + }) +} + +const DefaultNamespace = "default" + type ( APIResource struct { Category string diff --git a/pkg/object/mock/namespacer.go b/pkg/object/mock/namespacer.go new file mode 100644 index 0000000000..83809382aa --- /dev/null +++ b/pkg/object/mock/namespacer.go @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2017, MegaEase + * All rights reserved. + * + * 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 mock implements some mock objects. +package mock + +import ( + "fmt" + "strings" + + "github.com/megaease/easegress/v2/pkg/api" + "github.com/megaease/easegress/v2/pkg/logger" + "github.com/megaease/easegress/v2/pkg/object/httpserver" + "github.com/megaease/easegress/v2/pkg/object/pipeline" + "github.com/megaease/easegress/v2/pkg/object/trafficcontroller" + "github.com/megaease/easegress/v2/pkg/supervisor" + "github.com/megaease/easegress/v2/pkg/util/codectool" +) + +const ( + // Category is the category of MockNamespacer. + Category = supervisor.CategoryBusinessController + // Kind is the kind of MockNamespacer + Kind = "MockNamespacer" +) + +func init() { + supervisor.Register(&MockNamespacer{}) + api.RegisterObject(&api.APIResource{ + Category: Category, + Kind: Kind, + Name: strings.ToLower(Kind), + Aliases: []string{"mocknamespacer"}, + }) +} + +type ( + // MockNamespacer create a mock namespace for test + MockNamespacer struct { + spec *Spec + tc *trafficcontroller.TrafficController + } + + // Spec is the MockNamespacer spec + Spec struct { + Namespace string `json:"namespace"` + HTTPServers []NamedHTTPServer `json:"httpServers"` + Pipelines []NamedPipeline `json:"pipelines"` + } + + NamedPipeline struct { + Kind string `json:"kind"` + Name string `json:"name"` + pipeline.Spec `json:",inline"` + } + + NamedHTTPServer struct { + Kind string `json:"kind"` + Name string `json:"name"` + httpserver.Spec `json:",inline"` + } +) + +// Validate validates the MockNamespacer Spec. +func (spec *Spec) Validate() error { + if spec.Namespace == "" { + return fmt.Errorf("namespace is empty") + } + for _, server := range spec.HTTPServers { + if server.Name == "" { + return fmt.Errorf("httpserver name is empty") + } + } + for _, pipeline := range spec.Pipelines { + if pipeline.Name == "" { + return fmt.Errorf("pipeline name is empty") + } + } + return nil +} + +// Category returns the category of MockNamespacer. +func (mn *MockNamespacer) Category() supervisor.ObjectCategory { + return Category +} + +// Kind returns the kind of MockNamespacer. +func (mn *MockNamespacer) Kind() string { + return Kind +} + +// DefaultSpec returns the default spec of MockNamespacer. +func (mn *MockNamespacer) DefaultSpec() interface{} { + return &Spec{} +} + +// Init initializes MockNamespacer. +func (mn *MockNamespacer) Init(superSpec *supervisor.Spec) { + spec := superSpec.ObjectSpec().(*Spec) + mn.spec = spec + mn.tc = getTrafficController(superSpec.Super()) + + for _, server := range mn.spec.HTTPServers { + jsonConfig, err := codectool.MarshalJSON(server) + if err != nil { + logger.Errorf("marshal httpserver failed: %v", err) + continue + } + s, err := supervisor.NewSpec(string(jsonConfig)) + if err != nil { + logger.Errorf("new httpserver spec failed: %v", err) + continue + } + mn.tc.CreateTrafficGateForSpec(spec.Namespace, s) + } + for _, pipeline := range mn.spec.Pipelines { + jsonConfig, err := codectool.MarshalJSON(pipeline) + if err != nil { + logger.Errorf("marshal pipeline failed: %v", err) + continue + } + s, err := supervisor.NewSpec(string(jsonConfig)) + if err != nil { + logger.Errorf("new pipeline spec failed: %v", err) + continue + } + mn.tc.CreatePipelineForSpec(spec.Namespace, s) + } +} + +// Inherit inherits previous generation of MockNamespacer. +func (mn *MockNamespacer) Inherit(superSpec *supervisor.Spec, previousGeneration supervisor.Object) { + previousGeneration.Close() + mn.Init(superSpec) +} + +func getTrafficController(super *supervisor.Supervisor) *trafficcontroller.TrafficController { + entity, exists := super.GetSystemController(trafficcontroller.Kind) + if !exists { + return nil + } + tc, ok := entity.Instance().(*trafficcontroller.TrafficController) + if !ok { + return nil + } + return tc +} + +// Status returns the status of MockNamespacer. +func (mn *MockNamespacer) Status() *supervisor.Status { + return &supervisor.Status{ + ObjectStatus: nil, + } +} + +// Close closes MockNamespacer. +func (mn *MockNamespacer) Close() { + for _, server := range mn.spec.HTTPServers { + mn.tc.DeleteTrafficGate(mn.spec.Namespace, server.Name) + } + for _, pipeline := range mn.spec.Pipelines { + mn.tc.DeletePipeline(mn.spec.Namespace, pipeline.Name) + } +} diff --git a/pkg/object/rawconfigtrafficcontroller/rawconfigtrafficcontroller.go b/pkg/object/rawconfigtrafficcontroller/rawconfigtrafficcontroller.go index 4314a8a3a0..0b844c8dd8 100644 --- a/pkg/object/rawconfigtrafficcontroller/rawconfigtrafficcontroller.go +++ b/pkg/object/rawconfigtrafficcontroller/rawconfigtrafficcontroller.go @@ -38,7 +38,7 @@ const ( Kind = "RawConfigTrafficController" // DefaultNamespace is the namespace of RawConfigTrafficController - DefaultNamespace = "default" + DefaultNamespace = api.DefaultNamespace ) type ( diff --git a/pkg/object/trafficcontroller/trafficcontroller.go b/pkg/object/trafficcontroller/trafficcontroller.go index 69e0343226..91dca675d5 100644 --- a/pkg/object/trafficcontroller/trafficcontroller.go +++ b/pkg/object/trafficcontroller/trafficcontroller.go @@ -21,10 +21,8 @@ package trafficcontroller import ( "fmt" "runtime/debug" - "strings" "sync" - "github.com/megaease/easegress/v2/pkg/api" "github.com/megaease/easegress/v2/pkg/cluster" "github.com/megaease/easegress/v2/pkg/context" "github.com/megaease/easegress/v2/pkg/logger" @@ -99,12 +97,6 @@ var _ easemonitor.Metricer = (*TrafficObjectStatus)(nil) func init() { supervisor.Register(&TrafficController{}) - api.RegisterObject(&api.APIResource{ - Category: Category, - Kind: Kind, - Name: strings.ToLower(Kind), - Aliases: []string{"trafficcontroller", "tc"}, - }) } // ToMetrics implements easemonitor.Metricer. @@ -714,3 +706,24 @@ func (tc *TrafficController) Close() { logger.Infof("delete namespace %s", name) } } + +// ListAllNamespace lists pipelines and traffic gates in all namespaces. +func (tc *TrafficController) ListAllNamespace() map[string][]*supervisor.ObjectEntity { + tc.mutex.Lock() + defer tc.mutex.Unlock() + + res := make(map[string][]*supervisor.ObjectEntity) + for namespace, space := range tc.namespaces { + entities := []*supervisor.ObjectEntity{} + space.pipelines.Range(func(k, v interface{}) bool { + entities = append(entities, v.(*supervisor.ObjectEntity)) + return true + }) + space.trafficGates.Range(func(k, v interface{}) bool { + entities = append(entities, v.(*supervisor.ObjectEntity)) + return true + }) + res[namespace] = entities + } + return res +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 992ccaaa09..9058977224 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -56,6 +56,7 @@ import ( _ "github.com/megaease/easegress/v2/pkg/object/httpserver" _ "github.com/megaease/easegress/v2/pkg/object/ingresscontroller" _ "github.com/megaease/easegress/v2/pkg/object/meshcontroller" + _ "github.com/megaease/easegress/v2/pkg/object/mock" _ "github.com/megaease/easegress/v2/pkg/object/mqttproxy" _ "github.com/megaease/easegress/v2/pkg/object/nacosserviceregistry" _ "github.com/megaease/easegress/v2/pkg/object/pipeline"