From 248898e2ae266353002759d0d0c2d652ac2832f1 Mon Sep 17 00:00:00 2001 From: Weiqiang TANG Date: Mon, 20 Jan 2020 14:52:42 +0800 Subject: [PATCH] Refactor antctl framework - The antctl running against controller is consuming API server instead antctl server. - Updated yaml file to add essential RBAC to access corresponding APIs. - Add `get network-policy`, `get address-group` and `get applied-to-group` commands. Signed-off-by: Weiqiang TANG --- build/yamls/antrea-ipsec.yml | 22 +- build/yamls/antrea.yml | 22 +- build/yamls/base/antctl.yml | 18 +- build/yamls/base/controller.yml | 4 +- cmd/antctl/main.go | 10 +- cmd/antrea-agent/agent.go | 4 +- cmd/antrea-controller/controller.go | 14 +- go.mod | 2 - go.sum | 6 +- pkg/antctl/antctl.go | 119 +++++--- pkg/antctl/client.go | 90 +++--- pkg/antctl/command_definition.go | 265 ++++++++++-------- pkg/antctl/command_definition_test.go | 18 +- pkg/antctl/command_list.go | 86 ++---- pkg/antctl/command_list_test.go | 31 +- pkg/antctl/command_runtime.go | 42 +++ pkg/antctl/handlers/doc.go | 3 +- pkg/antctl/handlers/interface.go | 14 +- pkg/antctl/handlers/version.go | 22 +- pkg/antctl/handlers/version_test.go | 26 +- pkg/antctl/server.go | 36 +-- .../transform/addressgroup/transform.go | 62 ++++ .../transform/appliedtogroup/transform.go | 63 +++++ .../transform/networkpolicy/transform.go | 123 ++++++++ pkg/antctl/transform/version/transform.go | 72 +++++ test/e2e/antctl_test.go | 36 +-- 26 files changed, 776 insertions(+), 434 deletions(-) create mode 100644 pkg/antctl/command_runtime.go create mode 100644 pkg/antctl/transform/addressgroup/transform.go create mode 100644 pkg/antctl/transform/appliedtogroup/transform.go create mode 100644 pkg/antctl/transform/networkpolicy/transform.go create mode 100644 pkg/antctl/transform/version/transform.go diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml index 62ec1800b62..4ba57fa2ee0 100644 --- a/build/yamls/antrea-ipsec.yml +++ b/build/yamls/antrea-ipsec.yml @@ -69,11 +69,23 @@ metadata: app: antrea name: antctl rules: -- nonResourceURLs: - - /apis/system.antrea.tanzu.vmware.com - - /apis/system.antrea.tanzu.vmware.com/* +- apiGroups: + - clusterinformation.antrea.tanzu.vmware.com + resourceNames: + - antrea-controller + resources: + - antreacontrollerinfos verbs: - get +- apiGroups: + - networking.antrea.tanzu.vmware.com + resources: + - networkpolicies + - appliedtogroups + - addressgroups + verbs: + - get + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -379,9 +391,9 @@ kind: APIService metadata: labels: app: antrea - name: v1beta1.system.antrea.tanzu.vmware.com + name: v1beta1.networking.antrea.tanzu.vmware.com spec: - group: system.antrea.tanzu.vmware.com + group: networking.antrea.tanzu.vmware.com groupPriorityMinimum: 100 insecureSkipTLSVerify: true service: diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml index 3e6bf5ca06a..88faf2bebb9 100644 --- a/build/yamls/antrea.yml +++ b/build/yamls/antrea.yml @@ -69,11 +69,23 @@ metadata: app: antrea name: antctl rules: -- nonResourceURLs: - - /apis/system.antrea.tanzu.vmware.com - - /apis/system.antrea.tanzu.vmware.com/* +- apiGroups: + - clusterinformation.antrea.tanzu.vmware.com + resourceNames: + - antrea-controller + resources: + - antreacontrollerinfos verbs: - get +- apiGroups: + - networking.antrea.tanzu.vmware.com + resources: + - networkpolicies + - appliedtogroups + - addressgroups + verbs: + - get + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -370,9 +382,9 @@ kind: APIService metadata: labels: app: antrea - name: v1beta1.system.antrea.tanzu.vmware.com + name: v1beta1.networking.antrea.tanzu.vmware.com spec: - group: system.antrea.tanzu.vmware.com + group: networking.antrea.tanzu.vmware.com groupPriorityMinimum: 100 insecureSkipTLSVerify: true service: diff --git a/build/yamls/base/antctl.yml b/build/yamls/base/antctl.yml index 1d7a6288466..8eff9ce06e9 100644 --- a/build/yamls/base/antctl.yml +++ b/build/yamls/base/antctl.yml @@ -10,11 +10,23 @@ apiVersion: rbac.authorization.k8s.io/v1 metadata: name: antctl rules: - - nonResourceURLs: - - /apis/system.antrea.tanzu.vmware.com - - /apis/system.antrea.tanzu.vmware.com/* + - apiGroups: + - clusterinformation.antrea.tanzu.vmware.com + resources: + - antreacontrollerinfos + resourceNames: + - antrea-controller verbs: - get + - apiGroups: + - networking.antrea.tanzu.vmware.com + resources: + - networkpolicies + - appliedtogroups + - addressgroups + verbs: + - get + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/build/yamls/base/controller.yml b/build/yamls/base/controller.yml index bdb422e7d44..254f66834fb 100644 --- a/build/yamls/base/controller.yml +++ b/build/yamls/base/controller.yml @@ -14,10 +14,10 @@ spec: apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: - name: v1beta1.system.antrea.tanzu.vmware.com + name: v1beta1.networking.antrea.tanzu.vmware.com spec: insecureSkipTLSVerify: true - group: system.antrea.tanzu.vmware.com + group: networking.antrea.tanzu.vmware.com groupPriorityMinimum: 100 version: v1beta1 versionPriority: 100 diff --git a/cmd/antctl/main.go b/cmd/antctl/main.go index 00a4bb8a639..3272a41e511 100644 --- a/cmd/antctl/main.go +++ b/cmd/antctl/main.go @@ -18,7 +18,6 @@ import ( "flag" "os" "path" - "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -27,12 +26,7 @@ import ( "github.com/vmware-tanzu/antrea/pkg/antctl" ) -var ( - commandName = path.Base(os.Args[0]) - // TODO: May not work for antrea on windows - inPod = len(os.Getenv("POD_NAME")) != 0 - isAgent = strings.HasPrefix(os.Getenv("POD_NAME"), "antrea-agent") -) +var commandName = path.Base(os.Args[0]) var rootCmd = &cobra.Command{ Use: commandName, @@ -51,7 +45,7 @@ func main() { logs.InitLogs() defer logs.FlushLogs() - antctl.CommandList.ApplyToRootCommand(rootCmd, isAgent, inPod) + antctl.CommandList.ApplyToRootCommand(rootCmd) err := rootCmd.Execute() if err != nil { logs.FlushLogs() diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go index cf366a5e69e..54281fe1345 100644 --- a/cmd/antrea-agent/agent.go +++ b/cmd/antrea-agent/agent.go @@ -90,7 +90,7 @@ func run(o *Options) error { } nodeConfig := agentInitializer.GetNodeConfig() - antctlServer, err := antctl.NewLocalServer() + antctlServer, err := antctl.NewAgentServer() if err != nil { return fmt.Errorf("error when creating local antctl server: %w", err) } @@ -144,7 +144,7 @@ func run(o *Options) error { go agentMonitor.Run(stopCh) - antctlServer.Start(agentMonitor, nil, stopCh) + antctlServer.Start(agentMonitor, stopCh) <-stopCh klog.Info("Stopping Antrea agent") diff --git a/cmd/antrea-controller/controller.go b/cmd/antrea-controller/controller.go index 9fa3cc7cae3..07b63cf3cea 100644 --- a/cmd/antrea-controller/controller.go +++ b/cmd/antrea-controller/controller.go @@ -24,7 +24,6 @@ import ( "k8s.io/client-go/informers" "k8s.io/klog" - "github.com/vmware-tanzu/antrea/pkg/antctl" "github.com/vmware-tanzu/antrea/pkg/apiserver" "github.com/vmware-tanzu/antrea/pkg/apiserver/storage" "github.com/vmware-tanzu/antrea/pkg/controller/networkpolicy" @@ -78,11 +77,6 @@ func run(o *Options) error { return fmt.Errorf("error creating API server: %v", err) } - antctlServer, err := antctl.NewLocalServer() - if err != nil { - return fmt.Errorf("error when creating local antctl server: %w", err) - } - // set up signal capture: the first SIGTERM / SIGINT signal is handled gracefully and will // cause the stopCh channel to be closed; if another signal is received before the program // exits, we will force exit. @@ -95,13 +89,7 @@ func run(o *Options) error { go networkPolicyController.Run(stopCh) - preparedAPIServer := apiServer.GenericAPIServer.PrepareRun() - // Set up the antctl handlers on the controller API server for remote access. - antctl.CommandList.InstallToAPIServer(preparedAPIServer.GenericAPIServer, controllerMonitor) - go preparedAPIServer.Run(stopCh) - - // Set up the antctl server for in-pod access. - antctlServer.Start(nil, controllerMonitor, stopCh) + go apiServer.GenericAPIServer.PrepareRun().Run(stopCh) <-stopCh klog.Info("Stopping Antrea controller") diff --git a/go.mod b/go.mod index 8a32f7fe5a7..69a02a2dce9 100644 --- a/go.mod +++ b/go.mod @@ -18,12 +18,10 @@ require ( github.com/gogo/protobuf v1.2.1 github.com/golang/mock v1.3.1 github.com/golang/protobuf v1.3.2 - github.com/google/gofuzz v1.0.0 // indirect github.com/google/uuid v1.1.1 github.com/googleapis/gnostic v0.3.1 // indirect github.com/imdario/mergo v0.3.7 // indirect github.com/j-keck/arping v1.0.0 - github.com/json-iterator/go v1.1.6 // indirect github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd github.com/satori/go.uuid v1.2.0 github.com/spf13/cobra v0.0.5 diff --git a/go.sum b/go.sum index 8f7db891f5b..d6f2be9df64 100644 --- a/go.sum +++ b/go.sum @@ -152,9 +152,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= @@ -208,9 +207,8 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c h1:XpRROA6ssPlTwJI8/pH+61uieOkcJhmAFz25cu0B94Y= github.com/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/juju/errors v0.0.0-20180806074554-22422dad46e1/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/testing v0.0.0-20190613124551-e81189438503/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 6f04c85b564..d80930a2c58 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -15,66 +15,93 @@ package antctl import ( - "encoding/json" - "io" - "io/ioutil" "reflect" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog" "github.com/vmware-tanzu/antrea/pkg/antctl/handlers" + "github.com/vmware-tanzu/antrea/pkg/antctl/transform/addressgroup" + "github.com/vmware-tanzu/antrea/pkg/antctl/transform/appliedtogroup" + "github.com/vmware-tanzu/antrea/pkg/antctl/transform/networkpolicy" + "github.com/vmware-tanzu/antrea/pkg/antctl/transform/version" + clusterinfov1beta1 "github.com/vmware-tanzu/antrea/pkg/apis/clusterinformation/v1beta1" + networkingv1beta1 "github.com/vmware-tanzu/antrea/pkg/apis/networking/v1beta1" "github.com/vmware-tanzu/antrea/pkg/client/clientset/versioned/scheme" - "github.com/vmware-tanzu/antrea/pkg/version" ) -// unixDomainSockAddr is the address for antctl server in local mode. +// unixDomainSockAddr is the address for antctl server running alongside with antrea-agent. const unixDomainSockAddr = "/var/run/antctl.sock" -var systemGroup = schema.GroupVersion{Group: "system.antrea.tanzu.vmware.com", Version: "v1beta1"} - -type transformedVersionResponse struct { - handlers.ComponentVersionResponse `json:",inline" yaml:",inline"` - AntctlVersion string `json:"antctlVersion" yaml:"antctlVersion"` -} - -// versionTransform is the AddonTransform for the version command. This function -// will try to parse the response as a ComponentVersionResponse and then populate -// it with the version of antctl to a transformedVersionResponse object. -func versionTransform(reader io.Reader, _ bool) (interface{}, error) { - b, err := ioutil.ReadAll(reader) - if err != nil { - return nil, err - } - klog.Infof("version transform received: %s", string(b)) - cv := new(handlers.ComponentVersionResponse) - err = json.Unmarshal(b, cv) - if err != nil { - return nil, err - } - resp := &transformedVersionResponse{ - ComponentVersionResponse: *cv, - AntctlVersion: version.GetFullVersion(), - } - return resp, nil -} - -// CommandList defines all commands that could be used in the antctl for both agent +// CommandList defines all commands that could be used in the antctl for both agents // and controller. The unit test "TestCommandListValidation" ensures it to be valid. var CommandList = &commandList{ definitions: []commandDefinition{ { - Use: "version", - Short: "Print version information", - Long: "Print version information of the antctl and the ${component}", - HandlerFactory: new(handlers.Version), - GroupVersion: &systemGroup, - TransformedResponse: reflect.TypeOf(transformedVersionResponse{}), - Agent: true, - Controller: true, - SingleObject: true, - CommandGroup: flat, - AddonTransform: versionTransform, + use: "version", + short: "Print version information", + long: "Print version information of the antctl and the ${component}", + singleObject: true, + commandGroup: flat, + controllerEndpoint: &controllerEndpoint{ + resourceName: "antrea-controller", + groupVersionResource: &schema.GroupVersionResource{ + Group: clusterinfov1beta1.SchemeGroupVersion.Group, + Version: clusterinfov1beta1.SchemeGroupVersion.Version, + Resource: "antreacontrollerinfos", + }, + addonTransform: version.ControllerTransform, + }, + agentEndpoint: &agentEndpoint{ + HandlerFactory: new(handlers.Version), + addonTransform: version.AgentTransform, + }, + transformedResponse: reflect.TypeOf(version.Response{}), + }, + { + use: "network-policy", + short: "Print network policies", + long: "Print network policies in antrea controller", + commandGroup: get, + controllerEndpoint: &controllerEndpoint{ + groupVersionResource: &schema.GroupVersionResource{ + Group: networkingv1beta1.SchemeGroupVersion.Group, + Version: networkingv1beta1.SchemeGroupVersion.Version, + Resource: "networkpolicies", + }, + namespaced: true, + addonTransform: networkpolicy.Transform, + }, + transformedResponse: reflect.TypeOf(networkpolicy.Response{}), + }, + { + use: "applied-to-group", + short: "Print applied-to-groups", + long: "Print applied-to-groups in antrea controller", + commandGroup: get, + controllerEndpoint: &controllerEndpoint{ + groupVersionResource: &schema.GroupVersionResource{ + Group: networkingv1beta1.SchemeGroupVersion.Group, + Version: networkingv1beta1.SchemeGroupVersion.Version, + Resource: "appliedtogroups", + }, + addonTransform: appliedtogroup.Transform, + }, + transformedResponse: reflect.TypeOf(appliedtogroup.Response{}), + }, + { + use: "address-group", + short: "Print address groups", + long: "Print address groups in antrea controller", + commandGroup: get, + controllerEndpoint: &controllerEndpoint{ + groupVersionResource: &schema.GroupVersionResource{ + Group: networkingv1beta1.SchemeGroupVersion.Group, + Version: networkingv1beta1.SchemeGroupVersion.Version, + Resource: "addressgroups", + }, + addonTransform: addressgroup.Transform, + }, + transformedResponse: reflect.TypeOf(addressgroup.Response{}), }, }, codec: scheme.Codecs, diff --git a/pkg/antctl/client.go b/pkg/antctl/client.go index 684c8c0ed04..ad5c1f645ed 100644 --- a/pkg/antctl/client.go +++ b/pkg/antctl/client.go @@ -24,7 +24,6 @@ import ( "net/url" "time" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -33,17 +32,15 @@ import ( // requestOption describes options to issue requests to the antctl server. type requestOption struct { - groupVersion *schema.GroupVersion - // path is the destination URL of the ongoing request. - path *url.URL + commandDefinition *commandDefinition // kubeconfig is the path to the config file for kubectl. kubeconfig string - // name is the command which is going to be requested. - name string + // args are the parameters of the ongoing request. + args map[string]string // timeout specifies a time limit for requests made by the client. The timeout // duration includes connection setup, all redirects, and reading of the // response body. - timeOut time.Duration + timeout time.Duration } // client issues requests to an antctl server and gets the response. @@ -64,33 +61,40 @@ func (c *client) resolveKubeconfig(opt *requestOption) (*rest.Config, error) { if err != nil { return nil, err } - kubeconfig.GroupVersion = opt.groupVersion + gv := opt.commandDefinition.controllerEndpoint.groupVersionResource.GroupVersion() + kubeconfig.GroupVersion = &gv kubeconfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: c.codec} + kubeconfig.APIPath = "/apis" return kubeconfig, nil } -// localRequest issues the local request according to the requestOption. It only cares about -// the groupVersion of the requestOption which will be used to construct the request -// URI. localRequest is basically a raw http request, no authentication and authorization -// will be done during the request. For safety concerns, it communicates with the -// antctl server by a predefined unix domain socket. If the request succeeds, it -// will return an io.Reader which contains the response data. -func (c *client) localRequest(opt *requestOption) (io.Reader, error) { +// agentRequest issues the local request according to the requestOption. +// localRequest is basically a raw http request, no authentication and authorization +// will be done during the request. If the request succeeds, it will return an +// io.Reader which contains the response data. +func (c *client) agentRequest(opt *requestOption) (io.Reader, error) { client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (conn net.Conn, err error) { - if opt.timeOut != 0 { + if opt.timeout != 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, opt.timeOut) + ctx, cancel = context.WithTimeout(ctx, opt.timeout) defer cancel() } return new(net.Dialer).DialContext(ctx, "unix", unixDomainSockAddr) }, }, } - opt.path.Host = "antctl" - opt.path.Scheme = "http" - resp, err := client.Get(opt.path.String()) + u, _ := url.ParseRequestURI(opt.commandDefinition.agentRequestPath()) + q := u.Query() + for k, v := range opt.args { + q.Set(k, v) + } + u.RawQuery = q.Encode() + u.Scheme = "http" + u.Host = "antctl" + klog.Infoln("Issuing request to antrea agent:", u.String()) + resp, err := client.Get(u.String()) if err != nil { return nil, err } @@ -101,30 +105,33 @@ func (c *client) localRequest(opt *requestOption) (io.Reader, error) { return &buf, err } -// Request issues the appropriate request to the antctl server according to the -// request options. If the inPod field of the client is true, the client will do -// a local request by invoking localRequest. Otherwise, it will check the kubeconfig -// and delegate the request destined to the antctl server to the kubernetes API server. -// If the request succeeds, it will return an io.Reader which contains the response -// data. -func (c *client) Request(opt *requestOption) (io.Reader, error) { - if c.inPod { - klog.Infoln("antctl runs as local mode") - return c.localRequest(opt) - } +// controllerRequest issues request for the antctl running against antctl-controller. +// This function leverages k8s RestClient to do the request. +func (c *client) controllerRequest(opt *requestOption) (io.Reader, error) { + klog.Infoln("Issuing request to antrea controller") kubeconfig, err := c.resolveKubeconfig(opt) if err != nil { return nil, err } - restClient, err := rest.UnversionedRESTClientFor(kubeconfig) + restClient, err := rest.RESTClientFor(kubeconfig) if err != nil { return nil, fmt.Errorf("failed to create rest client: %w", err) } - // If timeout is not set, no timeout. - restClient.Client.Timeout = opt.timeOut - result := restClient.Get().RequestURI(opt.path.RequestURI()).Do() + // If timeout is zero, there will be no timeout. + restClient.Client.Timeout = opt.timeout + resGetter := restClient.Get(). + NamespaceIfScoped(opt.args["namespace"], opt.commandDefinition.controllerEndpoint.namespaced). + Resource(opt.commandDefinition.controllerEndpoint.groupVersionResource.Resource) + + if len(opt.commandDefinition.controllerEndpoint.resourceName) != 0 { + resGetter = resGetter.Name(opt.commandDefinition.controllerEndpoint.resourceName) + } else if name, ok := opt.args["name"]; ok { + resGetter = resGetter.Name(name) + } + + result := resGetter.Do() if result.Error() != nil { - return nil, fmt.Errorf("error when requesting %s: %w", opt.path.RequestURI(), result.Error()) + return nil, fmt.Errorf("error when requesting %s: %w", resGetter.URL(), result.Error()) } raw, err := result.Raw() if err != nil { @@ -132,3 +139,14 @@ func (c *client) Request(opt *requestOption) (io.Reader, error) { } return bytes.NewReader(raw), nil } + +// Request issues the appropriate request to the antctl server according to the +// request options. +// If the request succeeds, it will return an io.Reader which contains the response +// data. +func (c *client) Request(opt *requestOption) (io.Reader, error) { + if runtimeComponent == componentAgent { + return c.agentRequest(opt) + } + return c.controllerRequest(opt) +} diff --git a/pkg/antctl/command_definition.go b/pkg/antctl/command_definition.go index dfffdbfcd0a..86bace6437f 100644 --- a/pkg/antctl/command_definition.go +++ b/pkg/antctl/command_definition.go @@ -19,7 +19,6 @@ import ( "encoding/json" "fmt" "io" - "net/url" "os" "path" "reflect" @@ -82,10 +81,30 @@ const ( // argOption describes one argument which can be used in a requestOption. type argOption struct { - name string - fieldName string + key string usage string - key bool + optionals map[string]string +} + +// controllerEndpoint is used to specified the API for an antctl running against antrea-controller. +type controllerEndpoint struct { + groupVersionResource *schema.GroupVersionResource + resourceName string + namespaced bool + // AddonTransform is used to transform or update the response data received + // from the handler, it must returns an interface which has same type as + // TransformedResponse. + addonTransform func(reader io.Reader, single bool) (interface{}, error) +} + +// agentEndpoint is used to handle requests from an antctl running against antrea-agent. +type agentEndpoint struct { + // The handler factory of the command. + HandlerFactory handlers.Factory + // AddonTransform is used to transform or update the response data received + // from the handler, it must returns an interface which has same type as + // TransformedResponse. + addonTransform func(reader io.Reader, single bool) (interface{}, error) } // flagInfo represents a command-line flag that can be provided when invoking an antctl command. @@ -99,35 +118,20 @@ type flagInfo struct { // commandDefinition defines options to create a cobra.Command for an antctl client. type commandDefinition struct { // Cobra related - Use string // The lower value of it will be used as the endpoint path, like: /. - Short string - Long string - Example string // It will be filled with generated examples if it is not provided. + use string + short string + long string + example string // It will be filled with generated examples if it is not provided. // commandGroup represents the group of the command. - CommandGroup commandGroup - // Controller should be true if this command works for antrea-controller. - Controller bool - // Agent should be true if this command works for antrea-agent. - Agent bool - // SingleObject should be true if the handler always returns single object. The - // antctl assumes the response data as a slice of the objects by default. - SingleObject bool - // API is the endpoint for the command to retrieve data. - API string - // The handler factory of the command. - HandlerFactory handlers.Factory - // GroupVersion is the group version of the command handler, it should be set - // alongside with the HandlerFactory. - GroupVersion *schema.GroupVersion - // TransformedResponse is the final response struct of the command. If the + commandGroup commandGroup + controllerEndpoint *controllerEndpoint + agentEndpoint *agentEndpoint + singleObject bool + // transformedResponse is the final response struct of the command. If the // AddonTransform is set, TransformedResponse is not needed to be used as the // response struct of the handler, but it is still needed to guide the formatter. // It should always be filled. - TransformedResponse reflect.Type - // AddonTransform is used to transform or update the response data received - // from the handler, it must returns an interface which has same type as - // TransformedResponse. - AddonTransform func(reader io.Reader, single bool) (interface{}, error) + transformedResponse reflect.Type // Flags is a list of all possible flags for this command. Flags []flagInfo } @@ -135,25 +139,16 @@ type commandDefinition struct { // applySubCommandToRoot applies the commandDefinition to a cobra.Command with // the client. It populates basic fields of a cobra.Command and creates the // appropriate RunE function for it according to the commandDefinition. -func (cd *commandDefinition) applySubCommandToRoot(root *cobra.Command, client *client, isAgent bool) { +func (cd *commandDefinition) applySubCommandToRoot(root *cobra.Command, client *client) { cmd := &cobra.Command{ - Use: cd.Use, - Short: cd.Short, - Long: cd.Long, + Use: cd.use, + Short: cd.short, + Long: cd.long, } - renderDescription(cmd, isAgent) - + renderDescription(cmd) cd.applyFlagsToCommand(cmd) - argOpt := cd.argOption() - if argOpt != nil { - cmd.Args = cobra.MaximumNArgs(1) - cmd.Use += fmt.Sprintf(" [%s]", argOpt.name) - cmd.Long += "\n\nArgs:\n" + fmt.Sprintf(" %s\t%s", argOpt.name, argOpt.usage) - } else { - cmd.Args = cobra.NoArgs - } - if groupCommand, ok := groupCommands[cd.CommandGroup]; ok { + if groupCommand, ok := groupCommands[cd.commandGroup]; ok { groupCommand.AddCommand(cmd) } else { root.AddCommand(cmd) @@ -166,36 +161,33 @@ func (cd *commandDefinition) applySubCommandToRoot(root *cobra.Command, client * // validate checks if the commandDefinition is valid. func (cd *commandDefinition) validate() []error { var errs []error - if len(cd.Use) == 0 { + if len(cd.use) == 0 { errs = append(errs, fmt.Errorf("the command does not have name")) } - if cd.TransformedResponse == nil { - errs = append(errs, fmt.Errorf("%s: command does not define output struct", cd.Use)) - } - if !cd.Agent && !cd.Controller { - errs = append(errs, fmt.Errorf("%s: command does not define any supported component", cd.Use)) + if cd.transformedResponse == nil { + errs = append(errs, fmt.Errorf("%s: command does not define output struct", cd.use)) } - if cd.HandlerFactory == nil && len(cd.API) == 0 { - errs = append(errs, fmt.Errorf("%s: no handler or API specified", cd.Use)) + if cd.agentEndpoint == nil && cd.controllerEndpoint == nil { + errs = append(errs, fmt.Errorf("%s: command does not define any supported component", cd.use)) } - if len(cd.API) != 0 && cd.Agent { - errs = append(errs, fmt.Errorf("%s: commands for agent do not allow to request API directly", cd.Use)) + if cd.agentEndpoint != nil && cd.agentEndpoint.HandlerFactory == nil { + errs = append(errs, fmt.Errorf("%s: command for agent must define a handler as command endpoint", cd.use)) } - if cd.HandlerFactory != nil && cd.GroupVersion == nil { - errs = append(errs, fmt.Errorf("%s: must provide the group version of customize handler", cd.Use)) + if cd.controllerEndpoint != nil && cd.controllerEndpoint.groupVersionResource == nil { + errs = append(errs, fmt.Errorf("%s: command for controller must define an api resource as endpoint", cd.use)) } existingFlags := map[string]bool{"output": true, "help": true, "kubeconfig": true, "timeout": true, "verbose": true} for _, f := range cd.Flags { if len(f.name) == 0 { - errs = append(errs, fmt.Errorf("%s: flag name cannot be empty", cd.Use)) + errs = append(errs, fmt.Errorf("%s: flag name cannot be empty", cd.use)) } else { if _, ok := existingFlags[f.name]; ok { - errs = append(errs, fmt.Errorf("%s: flag redefined: %s", cd.Use, f.name)) + errs = append(errs, fmt.Errorf("%s: flag redefined: %s", cd.use, f.name)) } existingFlags[f.name] = true } if len(f.shorthand) > 1 { - errs = append(errs, fmt.Errorf("%s: length of a flag shorthand cannot be larger than 1: %s", cd.Use, f.shorthand)) + errs = append(errs, fmt.Errorf("%s: length of a flag shorthand cannot be larger than 1: %s", cd.use, f.shorthand)) } } return errs @@ -205,52 +197,54 @@ func (cd *commandDefinition) validate() []error { // exported field and return an argOption based on the first field annotated with // antctl. func (cd *commandDefinition) argOption() *argOption { - for i := 0; i < cd.TransformedResponse.NumField(); i++ { - f := cd.TransformedResponse.Field(i) + argOpt := &argOption{optionals: map[string]string{}} + for i := 0; i < cd.transformedResponse.NumField(); i++ { + f := cd.transformedResponse.Field(i) tags, err := structtag.Parse(string(f.Tag)) if err != nil { // Broken cli tags, skip this field continue } - argOpt := &argOption{fieldName: f.Name} + var name string jsonTag, err := tags.Get("json") if err != nil { - argOpt.name = strings.ToLower(f.Name) + name = strings.ToLower(f.Name) } else { - argOpt.name = jsonTag.Name + name = jsonTag.Name } cliTag, err := tags.Get(tagKey) - if err != nil { + if err != nil { // Broken cli tags, skip this field continue } - argOpt.key = cliTag.Name == tagOptionKeyArg - argOpt.usage = strings.Join(cliTag.Options, ", ") - return argOpt + if cliTag.Name != tagOptionKeyArg { + argOpt.optionals[name] = strings.Join(cliTag.Options, ", ") + } else { + argOpt.key = strings.ToLower(f.Name) + argOpt.usage = strings.Join(cliTag.Options, ", ") + } } - return nil + return argOpt } // decode parses the data in reader and converts it to one or more // TransformedResponse objects. If single is false, the return type is // []TransformedResponse. Otherwise, the return type is TransformedResponse. func (cd *commandDefinition) decode(r io.Reader, single bool) (interface{}, error) { - var result interface{} - if single || cd.SingleObject { - ref := reflect.New(cd.TransformedResponse) - err := json.NewDecoder(r).Decode(ref.Interface()) - if err != nil { - return nil, err - } - result = ref.Interface() + var refType reflect.Type + if single { + refType = cd.transformedResponse } else { - ref := reflect.New(reflect.SliceOf(cd.TransformedResponse)) - err := json.NewDecoder(r).Decode(ref.Interface()) - if err != nil { - return nil, err - } - result = reflect.Indirect(ref).Interface() + refType = reflect.SliceOf(cd.transformedResponse) + } + ref := reflect.New(refType) + err := json.NewDecoder(r).Decode(ref.Interface()) + if err != nil { + return nil, err + } + if single { + return ref.Interface(), nil } - return result, nil + return reflect.Indirect(ref).Interface(), nil } func jsonEncode(obj interface{}, output *bytes.Buffer) error { @@ -385,13 +379,21 @@ func (cd *commandDefinition) tableOutput(obj interface{}, writer io.Writer) erro // doing transform. func (cd *commandDefinition) output(resp io.Reader, writer io.Writer, ft formatterType, single bool) (err error) { var obj interface{} - if cd.AddonTransform == nil { // Decode the data if there is no AddonTransform. + + var addonTransform func(reader io.Reader, single bool) (interface{}, error) + if runtimeComponent == componentController && cd.controllerEndpoint.addonTransform != nil { + addonTransform = cd.controllerEndpoint.addonTransform + } else if runtimeComponent == componentAgent && cd.agentEndpoint.addonTransform != nil { + addonTransform = cd.agentEndpoint.addonTransform + } + + if addonTransform == nil { // Decode the data if there is no AddonTransform. obj, err = cd.decode(resp, single) if err != nil { return fmt.Errorf("error when decoding response: %w", err) } } else { - obj, err = cd.AddonTransform(resp, single) + obj, err = addonTransform(resp, single) if err != nil { return fmt.Errorf("error when doing local transform: %w", err) } @@ -412,43 +414,41 @@ func (cd *commandDefinition) output(resp io.Reader, writer io.Writer, ft formatt } // newCommandRunE creates the RunE function for the command. The RunE function -// checks the args according to ArgOptions and flags. +// checks the args according to argOption and flags. func (cd *commandDefinition) newCommandRunE(c *client) func(*cobra.Command, []string) error { + argOpt := cd.argOption() return func(cmd *cobra.Command, args []string) error { - kubeconfigPath, _ := cmd.Flags().GetString("kubeconfig") - timeout, _ := cmd.Flags().GetDuration("timeout") - u := new(url.URL) - if len(cd.API) > 0 { - u.Path = cd.API - } else { - u.Path = path.Join("/apis", cd.GroupVersion.String(), strings.ToLower(cd.Use)) - } - - q := u.Query() + argMap := make(map[string]string) if len(args) > 0 { - q.Add(cd.argOption().name, args[0]) + argMap[argOpt.key] = args[0] } for _, f := range cd.Flags { v, err := cmd.Flags().GetString(f.name) - if err != nil { - return err + if err == nil && len(v) != 0 { + argMap[f.name] = v } - q.Add(f.name, v) } - u.RawQuery = q.Encode() - - klog.Infof("Requesting URI %s", u.RequestURI()) + for flag := range argOpt.optionals { + val, err := cmd.Flags().GetString(flag) + if err == nil && len(val) != 0 { + argMap[flag] = val + } + } + if cd.controllerEndpoint != nil && cd.controllerEndpoint.namespaced { + argMap["namespace"], _ = cmd.Flags().GetString("namespace") + } + kubeconfigPath, _ := cmd.Flags().GetString("kubeconfig") + timeout, _ := cmd.Flags().GetDuration("timeout") resp, err := c.Request(&requestOption{ - path: u, - kubeconfig: kubeconfigPath, - name: cmd.Name(), - timeOut: timeout, - groupVersion: cd.GroupVersion, + commandDefinition: cd, + kubeconfig: kubeconfigPath, + args: argMap, + timeout: timeout, }) if err != nil { return err } - single := len(args) > 0 + single := len(args) != 0 || (cd.controllerEndpoint != nil && len(cd.controllerEndpoint.resourceName) != 0) outputFormat, err := cmd.Flags().GetString("output") if err != nil { return err @@ -457,26 +457,37 @@ func (cd *commandDefinition) newCommandRunE(c *client) func(*cobra.Command, []st } } -// applyFlagsToCommand sets up flags for the command. +// applyFlagsToCommand sets up args and flags for the command. func (cd *commandDefinition) applyFlagsToCommand(cmd *cobra.Command) { + argOpt := cd.argOption() + if len(argOpt.key) != 0 { + cmd.Args = cobra.MaximumNArgs(1) + cmd.Use += fmt.Sprintf(" [%s]", argOpt.key) + cmd.Long += "\n\nArgs:\n" + fmt.Sprintf(" %s\t%s", argOpt.key, argOpt.usage) + } else { + cmd.Args = cobra.NoArgs + } + for arg, usage := range argOpt.optionals { + cmd.Flags().String(arg, "", usage) + } + cmd.Flags().StringP("output", "o", "json", "output format: json|yaml|table") + if cd.controllerEndpoint != nil && cd.controllerEndpoint.namespaced { + cmd.Flags().StringP("namespace", "n", "default", "specify the namespace of the resource") + } for _, f := range cd.Flags { cmd.Flags().StringP(f.name, f.shorthand, f.defaultValue, f.usage) } } -func (cd *commandDefinition) requestPath(prefix string) string { - return path.Join(prefix, strings.ToLower(cd.Use)) -} - // applyExampleToCommand generates examples according to the commandDefinition. -// It only creates for commands which specified TransformedResponse. If the SingleObject +// It only creates for commands which specified TransformedResponse. If the singleObject // is specified, it only creates one example to retrieve the single object. Otherwise, // it will generates examples about retrieving single object according to the key // argOption and retrieving the object list. func (cd *commandDefinition) applyExampleToCommand(cmd *cobra.Command) { - if len(cd.Example) != 0 { - cmd.Example = cd.Example + if len(cd.example) != 0 { + cmd.Example = cd.example return } @@ -489,17 +500,16 @@ func (cd *commandDefinition) applyExampleToCommand(cmd *cobra.Command) { } var buf bytes.Buffer - typeName := cd.TransformedResponse.Name() - dataName := strings.ToLower(strings.TrimSuffix(typeName, "Response")) + dataName := strings.ToLower(cd.use) - if cd.SingleObject { + if cd.singleObject { fmt.Fprintf(&buf, " Get the %s\n", dataName) fmt.Fprintf(&buf, " $ %s\n", strings.Join(commands, " ")) } else { - key := cd.argOption() - if key != nil { + argOpt := cd.argOption() + if len(argOpt.key) != 0 { fmt.Fprintf(&buf, " Get a %s\n", dataName) - fmt.Fprintf(&buf, " $ %s [%s]\n", strings.Join(commands, " "), key.name) + fmt.Fprintf(&buf, " $ %s [%s]\n", strings.Join(commands, " "), argOpt.key) } fmt.Fprintf(&buf, " Get the list of %s\n", dataName) fmt.Fprintf(&buf, " $ %s\n", strings.Join(commands, " ")) @@ -507,3 +517,10 @@ func (cd *commandDefinition) applyExampleToCommand(cmd *cobra.Command) { cmd.Example = buf.String() } + +func (cd *commandDefinition) agentRequestPath() string { + if cd.agentEndpoint == nil { + return "" + } + return path.Join("/antctl", cd.use) +} diff --git a/pkg/antctl/command_definition_test.go b/pkg/antctl/command_definition_test.go index 269962b1390..c8d797b3a42 100644 --- a/pkg/antctl/command_definition_test.go +++ b/pkg/antctl/command_definition_test.go @@ -87,9 +87,10 @@ func TestFormat(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { opt := &commandDefinition{ - SingleObject: tc.singleton, - TransformedResponse: tc.responseStruct, - AddonTransform: tc.transform, + singleObject: tc.singleton, + transformedResponse: tc.responseStruct, + controllerEndpoint: &controllerEndpoint{addonTransform: tc.transform}, + agentEndpoint: &agentEndpoint{addonTransform: tc.transform}, } var responseData []byte responseData, err := json.Marshal(tc.rawResponseData) @@ -126,19 +127,19 @@ func TestCommandDefinitionGenerateExample(t *testing.T) { cmdChain: "first second third", singleObject: true, responseType: reflect.TypeOf(fooResponse{}), - expect: " Get the foo\n $ first second third test\n", + expect: " Get the test\n $ first second third test\n", }, "NoKeyList": { use: "test", cmdChain: "first second third", responseType: reflect.TypeOf(fooResponse{}), - expect: " Get the list of foo\n $ first second third test\n", + expect: " Get the list of test\n $ first second third test\n", }, "KeyList": { use: "test", cmdChain: "first second third", responseType: reflect.TypeOf(keyFooResponse{}), - expect: " Get a keyfoo\n $ first second third test [bar]\n Get the list of keyfoo\n $ first second third test\n", + expect: " Get a test\n $ first second third test [bar]\n Get the list of test\n $ first second third test\n", }, } { t.Run(k, func(t *testing.T) { @@ -152,8 +153,9 @@ func TestCommandDefinitionGenerateExample(t *testing.T) { cmd.Use = tc.use co := &commandDefinition{ - SingleObject: tc.singleObject, - TransformedResponse: tc.responseType, + use: tc.use, + singleObject: tc.singleObject, + transformedResponse: tc.responseType, } co.applyExampleToCommand(cmd) assert.Equal(t, tc.expect, cmd.Example) diff --git a/pkg/antctl/command_list.go b/pkg/antctl/command_list.go index 126e563d585..beeb60e6c01 100644 --- a/pkg/antctl/command_list.go +++ b/pkg/antctl/command_list.go @@ -15,18 +15,14 @@ package antctl import ( - "encoding/json" "flag" "fmt" "math" - "net/http" "path/filepath" "strings" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/mux" "k8s.io/client-go/util/homedir" "k8s.io/klog" @@ -34,66 +30,27 @@ import ( "github.com/vmware-tanzu/antrea/pkg/monitor" ) -// commandList organizes definitions. +// commandList organizes commands definitions. // It is the protocol for a pair of antctl client and server. type commandList struct { definitions []commandDefinition codec serializer.CodecFactory } -func (cl *commandList) InstallToAPIServer(apiServer *server.GenericAPIServer, cq monitor.ControllerQuerier) { - cl.applyToMux(apiServer.Handler.NonGoRestfulMux, nil, cq) -} - // applyToMux adds the handler function of each commandDefinition in the -// commandList to the mux with path /apis//. It also adds -// corresponding discovery handlers at /apis/ for kubernetes service -// discovery. -func (cl *commandList) applyToMux(mux *mux.PathRecorderMux, aq monitor.AgentQuerier, cq monitor.ControllerQuerier) { - resources := map[string][]metav1.APIResource{} +// commandList to the mux with path /. +func (cl *commandList) applyToMux(mux *mux.PathRecorderMux, aq monitor.AgentQuerier) { for _, def := range cl.definitions { - if def.HandlerFactory == nil { + if def.agentEndpoint == nil { continue } - handler := def.HandlerFactory.Handler(aq, cq) - groupPath := "/apis/" + def.GroupVersion.String() - reqPath := def.requestPath(groupPath) - klog.Infof("Adding cli handler %s", reqPath) - mux.HandleFunc(reqPath, handler) - resources[groupPath] = append(resources[groupPath], metav1.APIResource{ - Name: def.Use, - SingularName: def.Use, - Kind: def.Use, - Namespaced: false, - Group: def.GroupVersion.Group, - Version: def.GroupVersion.Version, - Verbs: metav1.Verbs{"get"}, - }) - } - // Setup up discovery handlers for command handlers. - for groupPath, resource := range resources { - mux.HandleFunc(groupPath, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - l := metav1.APIResourceList{ - TypeMeta: metav1.TypeMeta{Kind: "APIResourceList", APIVersion: metav1.SchemeGroupVersion.Version}, - GroupVersion: systemGroup.Version, - APIResources: resource, - } - jsonResp, err := json.MarshalIndent(l, "", " ") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - _, err = w.Write(jsonResp) - if err != nil { - klog.Errorf("Error when responding APIResourceList: %v", err) - w.WriteHeader(http.StatusInternalServerError) - } - }) + handler := def.agentEndpoint.HandlerFactory.Handler(aq) + klog.Infof("Adding cli handler %s", def.agentRequestPath()) + mux.HandleFunc(def.agentRequestPath(), handler) } } -func (cl *commandList) applyFlagsToRootCommand(root *cobra.Command) { +func (cl *commandList) applyPersistentFlagsToRoot(root *cobra.Command) { defaultKubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config") root.PersistentFlags().BoolP("verbose", "v", false, "enable verbose output") root.PersistentFlags().StringP("kubeconfig", "k", defaultKubeconfig, "absolute path to the kubeconfig file") @@ -102,7 +59,7 @@ func (cl *commandList) applyFlagsToRootCommand(root *cobra.Command) { // ApplyToRootCommand applies the commandList to the root cobra command, it applies // each commandDefinition of it to the root command as a sub-command. -func (cl *commandList) ApplyToRootCommand(root *cobra.Command, isAgent bool, inPod bool) { +func (cl *commandList) ApplyToRootCommand(root *cobra.Command) { client := &client{ inPod: inPod, codec: cl.codec, @@ -112,13 +69,14 @@ func (cl *commandList) ApplyToRootCommand(root *cobra.Command, isAgent bool, inP } for i := range cl.definitions { def := cl.definitions[i] - if (def.Agent != isAgent) && (def.Controller != !isAgent) { + if (runtimeComponent == componentAgent && def.agentEndpoint == nil) || + (runtimeComponent == componentController && def.controllerEndpoint == nil) { continue } - def.applySubCommandToRoot(root, client, isAgent) - klog.Infof("Added command %s", def.Use) + def.applySubCommandToRoot(root, client) + klog.Infof("Added command %s", def.use) } - cl.applyFlagsToRootCommand(root) + cl.applyPersistentFlagsToRoot(root) root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { enableVerbose, err := root.PersistentFlags().GetBool("verbose") if err != nil { @@ -136,7 +94,7 @@ func (cl *commandList) ApplyToRootCommand(root *cobra.Command, isAgent bool, inP } return nil } - renderDescription(root, isAgent) + renderDescription(root) } // validate checks the validation of the commandList. @@ -147,7 +105,7 @@ func (cl *commandList) validate() []error { } for i, c := range cl.definitions { for _, err := range c.validate() { - errs = append(errs, fmt.Errorf("#%d command<%s>: %w", i, c.Use, err)) + errs = append(errs, fmt.Errorf("#%d command<%s>: %w", i, c.use, err)) } } return errs @@ -155,13 +113,7 @@ func (cl *commandList) validate() []error { // renderDescription replaces placeholders ${component} in Short and Long of a command // to the determined component during runtime. -func renderDescription(command *cobra.Command, isAgent bool) { - var componentName string - if isAgent { - componentName = "agent" - } else { - componentName = "controller" - } - command.Short = strings.ReplaceAll(command.Short, "${component}", componentName) - command.Long = strings.ReplaceAll(command.Long, "${component}", componentName) +func renderDescription(command *cobra.Command) { + command.Short = strings.ReplaceAll(command.Short, "${component}", string(runtimeComponent)) + command.Long = strings.ReplaceAll(command.Long, "${component}", string(runtimeComponent)) } diff --git a/pkg/antctl/command_list_test.go b/pkg/antctl/command_list_test.go index 1bf791a7b0f..630868b5ef0 100644 --- a/pkg/antctl/command_list_test.go +++ b/pkg/antctl/command_list_test.go @@ -24,7 +24,6 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/server/mux" "github.com/vmware-tanzu/antrea/pkg/client/clientset/versioned/scheme" @@ -33,7 +32,7 @@ import ( type testHandlerFactory struct{} -func (t *testHandlerFactory) Handler(_ monitor.AgentQuerier, _ monitor.ControllerQuerier) http.HandlerFunc { +func (t *testHandlerFactory) Handler(_ monitor.AgentQuerier) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() fmt.Fprint(w, "test") @@ -48,17 +47,13 @@ type testResponse struct { var testCommandList = &commandList{ definitions: []commandDefinition{ { - Use: "test", - Short: "test short description ${component}", - Long: "test description ${component}", - HandlerFactory: new(testHandlerFactory), - TransformedResponse: reflect.TypeOf(testResponse{}), - Agent: true, - Controller: true, - GroupVersion: &schema.GroupVersion{ - Group: "test-clusterinformation.antrea.tanzu.vmware.com", - Version: "v1", + use: "test", + short: "test short description ${component}", + long: "test description ${component}", + agentEndpoint: &agentEndpoint{ + HandlerFactory: new(testHandlerFactory), }, + transformedResponse: reflect.TypeOf(testResponse{}), }, }, codec: scheme.Codecs, @@ -68,19 +63,19 @@ func TestCommandListApplyToCommand(t *testing.T) { testRoot := new(cobra.Command) testRoot.Short = "The component is ${component}" testRoot.Long = "The component is ${component}" - testCommandList.ApplyToRootCommand(testRoot, true, false) + testCommandList.ApplyToRootCommand(testRoot) // sub-commands should be attached assert.True(t, testRoot.HasSubCommands()) // render should work as expected - assert.Contains(t, testRoot.Short, "The component is agent") - assert.Contains(t, testRoot.Long, "The component is agent") + assert.Contains(t, testRoot.Short, "The component is controller") + assert.Contains(t, testRoot.Long, "The component is controller") } // TestParseCommandList ensures the commandList could be correctly parsed. func TestParseCommandList(t *testing.T) { - r := mux.NewPathRecorderMux("") + r := mux.NewPathRecorderMux("antctl-test") assert.Len(t, testCommandList.validate(), 0) - testCommandList.applyToMux(r, nil, nil) + testCommandList.applyToMux(r, nil) ts := httptest.NewServer(r) defer ts.Close() @@ -90,7 +85,7 @@ func TestParseCommandList(t *testing.T) { statusCode int }{ "ExistPath": { - path: "/apis/" + testCommandList.definitions[0].GroupVersion.String() + "/test", + path: testCommandList.definitions[0].agentRequestPath(), statusCode: http.StatusOK, }, "NotExistPath": { diff --git a/pkg/antctl/command_runtime.go b/pkg/antctl/command_runtime.go new file mode 100644 index 00000000000..c598509a82f --- /dev/null +++ b/pkg/antctl/command_runtime.go @@ -0,0 +1,42 @@ +// Copyright 2020 Antrea 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 antctl + +import ( + "os" + "strings" +) + +const ( + componentController string = "controller" + componentAgent string = "agent" +) + +var ( + // runtimeComponent tells the component the antctl client running against or + // the antctl server running in. + runtimeComponent string + // inPod tells if the antctl client is running in a Pod. + inPod bool +) + +func init() { + inPod = len(os.Getenv("POD_NAME")) != 0 + if strings.HasPrefix(os.Getenv("POD_NAME"), "antrea-agent") { + runtimeComponent = componentAgent + } else { + runtimeComponent = componentController + } +} diff --git a/pkg/antctl/handlers/doc.go b/pkg/antctl/handlers/doc.go index ed74d9b8b80..3e501289d64 100644 --- a/pkg/antctl/handlers/doc.go +++ b/pkg/antctl/handlers/doc.go @@ -12,5 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package handlers contains handler implementations for different antctl commands. +// Package handlers contains agent side handler implementations for different +// antctl commands. package handlers diff --git a/pkg/antctl/handlers/interface.go b/pkg/antctl/handlers/interface.go index 8cdc9f47a8d..a08238a75ab 100644 --- a/pkg/antctl/handlers/interface.go +++ b/pkg/antctl/handlers/interface.go @@ -20,14 +20,12 @@ import ( "github.com/vmware-tanzu/antrea/pkg/monitor" ) -// Factory is the interface to generate command handlers. +// Factory is the interface to generate handlers. The handlers will be used to +// serve requests from the antctl binary which runs against agents. type Factory interface { // Handler returns a net/http.HandlerFunc which will be used to handle - // requests issued by commands from the antctl client. An implementation - // needs to determine the component it is running in by checking nullable - // of the AgentQuerier or the ControllerQuerier. If the antctl server is - // running in the antrea-agent, the AgentQuerier will not be nil, otherwise, - // the ControllerQuerier will not be nil. If the command has no AddonTransform, - // the HandlerFunc need to write the data to the response body in JSON format. - Handler(aq monitor.AgentQuerier, cq monitor.ControllerQuerier) http.HandlerFunc + // requests issued by commands from the agent antctl client. + // If the command has no AddonTransform, the HandlerFunc need to write the + // data to the response body in JSON format. + Handler(aq monitor.AgentQuerier) http.HandlerFunc } diff --git a/pkg/antctl/handlers/version.go b/pkg/antctl/handlers/version.go index ef5ec3da577..6dc3c5bc936 100644 --- a/pkg/antctl/handlers/version.go +++ b/pkg/antctl/handlers/version.go @@ -25,33 +25,29 @@ import ( var _ Factory = new(Version) -// ComponentVersionResponse describes the internal response struct of the version -// command. It contains the version of the component the antctl server is running -// in, either the agent or the controller. +// AgentVersionResponse describes the internal response struct of the version +// command. // This struct is not the final response struct of the version command. The version // command definition has an AddonTransform which will populate this struct and the // version of antctl client to the final response. -type ComponentVersionResponse struct { - AgentVersion string `json:"agentVersion,omitempty" yaml:"agentVersion,omitempty"` - ControllerVersion string `json:"controllerVersion,omitempty" yaml:"controllerVersion,omitempty"` +type AgentVersionResponse struct { + AgentVersion string `json:"agentVersion,omitempty" yaml:"agentVersion,omitempty"` } -// Version is Factory to generate version command handler. +// Version is the Factory which generates version command handler. type Version struct{} -// Handler returns the function which can handle queries issued by the version command, -func (v *Version) Handler(aq monitor.AgentQuerier, cq monitor.ControllerQuerier) http.HandlerFunc { +// Handler returns the function which can handle queries issued by the version command. +func (v *Version) Handler(aq monitor.AgentQuerier) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var m ComponentVersionResponse + var m AgentVersionResponse if aq != nil { m.AgentVersion = aq.GetVersion() - } else if cq != nil { - m.ControllerVersion = cq.GetVersion() } err := json.NewEncoder(w).Encode(m) if err != nil { w.WriteHeader(http.StatusInternalServerError) - klog.Errorf("Error when encoding ComponentVersionResponse to json: %v", err) + klog.Errorf("Error when encoding AgentVersionResponse to json: %v", err) } w.Header().Set("Content-Type", "application/json") } diff --git a/pkg/antctl/handlers/version_test.go b/pkg/antctl/handlers/version_test.go index ab7a8f8172a..097b48d63eb 100644 --- a/pkg/antctl/handlers/version_test.go +++ b/pkg/antctl/handlers/version_test.go @@ -30,22 +30,14 @@ func TestVersion(t *testing.T) { defer ctrl.Finish() testcases := map[string]struct { - version string - expectedOutput string - expectedStatusCode int - isAgent, isController bool + version string + expectedOutput string + expectedStatusCode int }{ "AgentVersion": { version: "v0.0.1", expectedOutput: "{\"agentVersion\":\"v0.0.1\"}\n", expectedStatusCode: http.StatusOK, - isAgent: true, - }, - "ControllerVersion": { - version: "v0.0.1", - expectedOutput: "{\"controllerVersion\":\"v0.0.1\"}\n", - expectedStatusCode: http.StatusOK, - isController: true, }, } for k, tc := range testcases { @@ -53,15 +45,9 @@ func TestVersion(t *testing.T) { req, err := http.NewRequest("GET", "/", nil) assert.Nil(t, err) recorder := httptest.NewRecorder() - if tc.isAgent { - aq := mockmonitor.NewMockAgentQuerier(ctrl) - aq.EXPECT().GetVersion().Return(tc.version) - new(Version).Handler(aq, nil).ServeHTTP(recorder, req) - } else if tc.isController { - cq := mockmonitor.NewMockControllerQuerier(ctrl) - cq.EXPECT().GetVersion().Return(tc.version) - new(Version).Handler(nil, cq).ServeHTTP(recorder, req) - } + aq := mockmonitor.NewMockAgentQuerier(ctrl) + aq.EXPECT().GetVersion().Return(tc.version) + new(Version).Handler(aq).ServeHTTP(recorder, req) assert.Equal(t, tc.expectedStatusCode, recorder.Code, k) assert.Equal(t, tc.expectedOutput, recorder.Body.String(), k) }) diff --git a/pkg/antctl/server.go b/pkg/antctl/server.go index df0e572b334..3efc13c5477 100644 --- a/pkg/antctl/server.go +++ b/pkg/antctl/server.go @@ -29,31 +29,22 @@ import ( "github.com/vmware-tanzu/antrea/pkg/monitor" ) -// Server defines operations of an antctl server. -type Server interface { - // Start runs the antctl server. When invoking this method, either AgentQuerier - // or ControllerQuerier must be passed, because implementations need to - // use the value of AgentMonitor and Controller monitor to tell out which - // component the server is running in. A running server can be stopped by - // closing the stopCh. - Start(aq monitor.AgentQuerier, cq monitor.ControllerQuerier, stopCh <-chan struct{}) -} - -type localServer struct { - // startOnce ensures the server could only be started one. +// AgentServer is the antctl server running in antrea agents serves in-pod antctl +// requests. +type AgentServer struct { + // startOnce ensures the server could only be started once. startOnce sync.Once listener net.Listener } -// Start starts the server with the AgentQuerier or the ControllerQuerier passed. -// The server will do graceful stop whenever it receives from the stopCh. One server -// could only be run once. -func (s *localServer) Start(aq monitor.AgentQuerier, cq monitor.ControllerQuerier, stopCh <-chan struct{}) { +// Start starts the AgentServer. It guarantees the server could only be started +// once. The server will do graceful stop whenever it receives from the stopCh. +func (s *AgentServer) Start(aq monitor.AgentQuerier, stopCh <-chan struct{}) { s.startOnce.Do(func() { antctlMux := mux.NewPathRecorderMux("antctl-server") - CommandList.applyToMux(antctlMux, aq, cq) + CommandList.applyToMux(antctlMux, aq) server := &http.Server{Handler: antctlMux} - // HTTP server graceful stop + // Graceful stop goroutine. go func() { <-stopCh err := server.Shutdown(context.Background()) @@ -63,7 +54,7 @@ func (s *localServer) Start(aq monitor.AgentQuerier, cq monitor.ControllerQuerie klog.Info("Antctl server stopped") } }() - // Start the http server + // Start the http server. go func() { klog.Info("Starting antctl server") err := server.Serve(s.listener) @@ -74,12 +65,13 @@ func (s *localServer) Start(aq monitor.AgentQuerier, cq monitor.ControllerQuerie }) } -// NewLocalServer creates an antctl server which listens on the local domain socket. -func NewLocalServer() (Server, error) { +// NewAgentServer creates an antctl server. For safety concerns, it creates the +// antctl server which listens on a predefined unix domain socket. +func NewAgentServer() (*AgentServer, error) { os.Remove(unixDomainSockAddr) ln, err := net.Listen("unix", unixDomainSockAddr) if err != nil { return nil, fmt.Errorf("error when creating antctl local server: %w", err) } - return &localServer{listener: ln}, nil + return &AgentServer{listener: ln}, nil } diff --git a/pkg/antctl/transform/addressgroup/transform.go b/pkg/antctl/transform/addressgroup/transform.go new file mode 100644 index 00000000000..9cdd21dbbde --- /dev/null +++ b/pkg/antctl/transform/addressgroup/transform.go @@ -0,0 +1,62 @@ +// Copyright 2020 Antrea 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 addressgroup + +import ( + "encoding/json" + "io" + "net" + "reflect" + + networkingv1beta1 "github.com/vmware-tanzu/antrea/pkg/apis/networking/v1beta1" +) + +type Response struct { + Name string `json:"name" yaml:"name"` + IPAddresses []string `json:"ipAddresses" yaml:"ipAddresses"` +} + +func listTransform(groups *networkingv1beta1.AddressGroupList) []Response { + result := []Response{} + for _, item := range groups.Items { + result = append(result, objectTransform(&item)) + } + return result +} + +func objectTransform(group *networkingv1beta1.AddressGroup) Response { + ips := []string{} + for _, ip := range group.IPAddresses { + ips = append(ips, net.IP(ip).String()) + } + return Response{Name: group.Name, IPAddresses: ips} +} + +func Transform(reader io.Reader, single bool) (interface{}, error) { + var refType reflect.Type + if single { + refType = reflect.TypeOf(networkingv1beta1.AddressGroup{}) + } else { + refType = reflect.TypeOf(networkingv1beta1.AddressGroupList{}) + } + refVal := reflect.New(refType) + if err := json.NewDecoder(reader).Decode(refVal.Interface()); err != nil { + return nil, err + } + if single { + return objectTransform(refVal.Interface().(*networkingv1beta1.AddressGroup)), nil + } + return listTransform(refVal.Interface().(*networkingv1beta1.AddressGroupList)), nil +} diff --git a/pkg/antctl/transform/appliedtogroup/transform.go b/pkg/antctl/transform/appliedtogroup/transform.go new file mode 100644 index 00000000000..d53b24ab9c6 --- /dev/null +++ b/pkg/antctl/transform/appliedtogroup/transform.go @@ -0,0 +1,63 @@ +// Copyright 2020 Antrea 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 appliedtogroup + +import ( + "encoding/json" + "io" + "reflect" + + networkingv1beta1 "github.com/vmware-tanzu/antrea/pkg/apis/networking/v1beta1" +) + +type Response struct { + Name string `json:"name" yaml:"name"` + Pods []networkingv1beta1.PodReference `json:"pods" yaml:"pods"` +} + +func transformList(groups *networkingv1beta1.AppliedToGroupList) []Response { + result := []Response{} + for _, group := range groups.Items { + result = append(result, transformObject(&group)) + } + return result +} + +func transformObject(group *networkingv1beta1.AppliedToGroup) Response { + if group.Pods == nil { + group.Pods = []networkingv1beta1.PodReference{} + } + return Response{ + Name: group.Name, + Pods: group.Pods, + } +} + +func Transform(reader io.Reader, single bool) (interface{}, error) { + var refType reflect.Type + if single { + refType = reflect.TypeOf(networkingv1beta1.AppliedToGroup{}) + } else { + refType = reflect.TypeOf(networkingv1beta1.AppliedToGroupList{}) + } + refVal := reflect.New(refType) + if err := json.NewDecoder(reader).Decode(refVal.Interface()); err != nil { + return nil, err + } + if single { + return transformObject(refVal.Interface().(*networkingv1beta1.AppliedToGroup)), nil + } + return transformList(refVal.Interface().(*networkingv1beta1.AppliedToGroupList)), nil +} diff --git a/pkg/antctl/transform/networkpolicy/transform.go b/pkg/antctl/transform/networkpolicy/transform.go new file mode 100644 index 00000000000..4716265a7b5 --- /dev/null +++ b/pkg/antctl/transform/networkpolicy/transform.go @@ -0,0 +1,123 @@ +// Copyright 2020 Antrea 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 networkpolicy + +import ( + "encoding/json" + "io" + "net" + "reflect" + + networkingv1beta1 "github.com/vmware-tanzu/antrea/pkg/apis/networking/v1beta1" +) + +type ipBlock struct { + CIDR string `json:"cidr" yaml:"cidr"` + Except []string `json:"except,omitempty" yaml:"except,omitempty"` +} + +type networkPolicyPeer struct { + AddressGroups []string `json:"addressGroups,omitempty" yaml:"addressGroups,omitempty"` + IPBlocks []ipBlock `json:"ipBlocks,omitempty" json:"ipBlocks,omitempty"` +} + +type networkPolicyRule struct { + Direction networkingv1beta1.Direction `json:"direction,omitempty" yaml:"direction,omitempty"` + From networkPolicyPeer `json:"from,omitempty" yaml:"from,omitempty"` + To networkPolicyPeer `json:"to,omitempty" yaml:"to,omitempty"` + Services []networkingv1beta1.Service `json:"services,omitempty" yaml:"services,omitempty"` +} + +type Response struct { + Name string `json:"name" yaml:"name"` + Rules []networkPolicyRule `json:"rules" yaml:"rules"` + AppliedToGroups []string `json:"appliedToGroups" yaml:"appliedToGroups"` +} + +func transformIPNet(ipNet networkingv1beta1.IPNet) *net.IPNet { + ip := net.IP(ipNet.IP) + var bits int + if ip.To4() != nil { + bits = net.IPv4len * 8 + } else { + bits = net.IPv6len * 8 + } + return &net.IPNet{IP: ip, Mask: net.CIDRMask(int(ipNet.PrefixLength), bits)} +} + +func transformIPBlock(block networkingv1beta1.IPBlock) ipBlock { + except := []string{} + for i := range block.Except { + except = append(except, transformIPNet(block.Except[i]).String()) + } + + return ipBlock{ + CIDR: transformIPNet(block.CIDR).String(), + Except: except, + } +} + +func transformNetworkPolicyPeer(peer networkingv1beta1.NetworkPolicyPeer) networkPolicyPeer { + blocks := []ipBlock{} + for _, originBlock := range peer.IPBlocks { + blocks = append(blocks, transformIPBlock(originBlock)) + } + return networkPolicyPeer{AddressGroups: peer.AddressGroups, IPBlocks: blocks} +} + +func transformObject(policy *networkingv1beta1.NetworkPolicy) *Response { + rules := []networkPolicyRule{} + for _, originRule := range policy.Rules { + rules = append(rules, networkPolicyRule{ + Direction: originRule.Direction, + From: transformNetworkPolicyPeer(originRule.From), + To: transformNetworkPolicyPeer(originRule.To), + Services: originRule.Services, + }) + } + if policy.AppliedToGroups == nil { + policy.AppliedToGroups = []string{} + } + return &Response{ + Name: policy.Name, + Rules: rules, + AppliedToGroups: policy.AppliedToGroups, + } +} + +func transformList(policyList *networkingv1beta1.NetworkPolicyList) []Response { + result := []Response{} + for _, item := range policyList.Items { + result = append(result, *transformObject(&item)) + } + return result +} + +func Transform(reader io.Reader, single bool) (interface{}, error) { + var refType reflect.Type + if single { + refType = reflect.TypeOf(networkingv1beta1.NetworkPolicy{}) + } else { + refType = reflect.TypeOf(networkingv1beta1.NetworkPolicyList{}) + } + refVal := reflect.New(refType) + if err := json.NewDecoder(reader).Decode(refVal.Interface()); err != nil { + return nil, err + } + if single { + return transformObject(refVal.Interface().(*networkingv1beta1.NetworkPolicy)), nil + } + return transformList(refVal.Interface().(*networkingv1beta1.NetworkPolicyList)), nil +} diff --git a/pkg/antctl/transform/version/transform.go b/pkg/antctl/transform/version/transform.go new file mode 100644 index 00000000000..28646ec287e --- /dev/null +++ b/pkg/antctl/transform/version/transform.go @@ -0,0 +1,72 @@ +// Copyright 2020 Antrea 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 version + +import ( + "encoding/json" + "io" + "io/ioutil" + + "k8s.io/klog" + + "github.com/vmware-tanzu/antrea/pkg/antctl/handlers" + clusterinfov1beta1 "github.com/vmware-tanzu/antrea/pkg/apis/clusterinformation/v1beta1" + pversion "github.com/vmware-tanzu/antrea/pkg/version" +) + +type Response struct { + AgentVersion string `json:"agentVersion,omitempty" yaml:"agentVersion,omitempty"` + ControllerVersion string `json:"controllerVersion,omitempty" yaml:"controllerVersion,omitempty"` + AntctlVersion string `json:"antctlVersion,omitempty" yaml:"antctlVersion,omitempty"` +} + +// AgentVersion is the AddonTransform for the version command. This function +// will try to parse the response as a AgentVersionResponse and then populate +// it with the version of antctl to a transformedVersionResponse object. +func AgentTransform(reader io.Reader, _ bool) (interface{}, error) { + b, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + klog.Infof("version transform received: %s", string(b)) + cv := new(handlers.AgentVersionResponse) + err = json.Unmarshal(b, cv) + if err != nil { + return nil, err + } + resp := &Response{ + AgentVersion: cv.AgentVersion, + AntctlVersion: pversion.GetFullVersion(), + } + return resp, nil +} + +func ControllerTransform(reader io.Reader, _ bool) (interface{}, error) { + b, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + klog.Infof("version transform received: %s", string(b)) + controllerInfo := new(clusterinfov1beta1.AntreaControllerInfo) + err = json.Unmarshal(b, controllerInfo) + if err != nil { + return nil, err + } + resp := &Response{ + ControllerVersion: controllerInfo.Version, + AntctlVersion: pversion.GetFullVersion(), + } + return resp, nil +} diff --git a/test/e2e/antctl_test.go b/test/e2e/antctl_test.go index de23e2c5f81..d8235b06fde 100644 --- a/test/e2e/antctl_test.go +++ b/test/e2e/antctl_test.go @@ -24,7 +24,7 @@ func runAntctl(podName string, subCMDs []string, data *TestData, tb testing.TB) } else { containerName = "antrea-controller" } - cmds := []string{"antctl", "-v"} + var cmds []string stdout, stderr, err := data.runCommandFromPod(antreaNamespace, podName, containerName, append(cmds, subCMDs...)) antctlOutput(stdout, stderr, tb) return stdout, stderr, err @@ -41,23 +41,7 @@ func TestAntctlAgentLocalAccess(t *testing.T) { if err != nil { t.Fatalf("Error when getting antrea-agent pod name: %v", err) } - if _, _, err := runAntctl(podName, []string{"version"}, data, t); err != nil { - t.Fatalf("Error when running `antctl version` from %s: %v", podName, err) - } -} - -// TestAntctlControllerLocalAccess ensures antctl is accessible in the controller Pod. -func TestAntctlControllerLocalAccess(t *testing.T) { - data, err := setupTest(t) - if err != nil { - t.Fatalf("Error when setting up test: %v", err) - } - defer teardownTest(t, data) - podName, err := data.getAntreaController() - if err != nil { - t.Fatalf("Error when getting antrea-controller pod name: %v", err) - } - if _, _, err := runAntctl(podName, []string{"version"}, data, t); err != nil { + if _, _, err := runAntctl(podName, []string{"antctl", "-v", "version"}, data, t); err != nil { t.Fatalf("Error when running `antctl version` from %s: %v", podName, err) } } @@ -71,7 +55,7 @@ func TestAntctlControllerRemoteAccess(t *testing.T) { t.Fatalf("Error when setting up test: %v", err) } defer teardownTest(t, data) - podName, err := data.getAntreaController() + podName, err := data.getAntreaPodOnNode(masterNodeName()) require.Nil(t, err, "Error when retrieving antrea controller pod name") // Copy antctl from the controller Pod to the master Node. @@ -85,21 +69,20 @@ func TestAntctlControllerRemoteAccess(t *testing.T) { require.Nil(t, err, "Error when make the antctl on master node executable, stdout: %s, stderr: %s", podName, stdout, stderr) for k, tc := range map[string]struct { - commands string + commands []string expectedReturnCode int }{ "CorrectConfig": { - commands: "-v version", + commands: []string{"~/antctl", "-v", "version"}, expectedReturnCode: 0, }, "MalformedConfig": { - commands: "-v version --kubeconfig /dev/null", + commands: []string{"~/antctl", "-v", "version", "--kubeconfig", "/dev/null"}, expectedReturnCode: 1, }, } { t.Run(k, func(t *testing.T) { - commands := "~/antctl " + tc.commands - rc, stdout, stderr, err = RunCommandOnNode(masterNodeName(), commands) + rc, stdout, stderr, err = RunCommandOnNode(masterNodeName(), strings.Join(tc.commands, " ")) antctlOutput(stdout, stderr, t) assert.Equal(t, tc.expectedReturnCode, rc) if err != nil { @@ -117,7 +100,7 @@ func TestAntctlVerboseMode(t *testing.T) { t.Fatalf("Error when setting up test: %v", err) } defer teardownTest(t, data) - podName, err := data.getAntreaController() + podName, err := data.getAntreaPodOnNode(masterNodeName()) require.Nil(t, err, "Error when retrieving antrea controller pod name") for _, tc := range []struct { name string @@ -128,11 +111,10 @@ func TestAntctlVerboseMode(t *testing.T) { {name: "RootVerbose", hasStderr: false, commands: []string{"antctl", "-v"}}, {name: "CommandNonVerbose", hasStderr: false, commands: []string{"antctl", "version"}}, {name: "CommandVerbose", hasStderr: true, commands: []string{"antctl", "-v", "version"}}, - {name: "CommandVerbose", hasStderr: true, commands: []string{"antctl", "version", "-v"}}, } { t.Run(tc.name, func(t *testing.T) { t.Logf("Running commnad `%s` on pod %s", tc.commands, podName) - stdout, stderr, err := data.runCommandFromPod(antreaNamespace, podName, "antrea-controller", tc.commands) + stdout, stderr, err := runAntctl(podName, tc.commands, data, t) antctlOutput(stdout, stderr, t) assert.Nil(t, err) if !tc.hasStderr {