diff --git a/Makefile b/Makefile index cf08d3d7..8823b981 100644 --- a/Makefile +++ b/Makefile @@ -95,19 +95,13 @@ build-helm: helm helm package charts/nebula-operator helm package charts/nebula-cluster mv nebula-operator-*.tgz nebula-cluster-*.tgz charts/ + cp config/crd/bases/apps.nebula-graph.io_nebulaclusters.yaml charts/nebula-operator/crds/nebulacluster.yaml run: run-controller-manager run-controller-manager: manifests generate check go run -ldflags '$(LDFLAGS)' cmd/controller-manager/main.go -build-helm: helm - helm repo index charts --url https://vesoft-inc.github.io/nebula-operator/charts - helm package charts/nebula-operator - helm package charts/nebula-cluster - mv nebula-operator-*.tgz nebula-cluster-*.tgz charts/ - cp config/crd/bases/apps.nebula-graph.io_nebulaclusters.yaml charts/nebula-operator/crds/nebulacluster.yaml - run-scheduler: manifests generate check go run -ldflags '$(LDFLAGS)' cmd/scheduler/main.go diff --git a/cmd/ngctl/main.go b/cmd/ngctl/main.go new file mode 100644 index 00000000..f3d40e9a --- /dev/null +++ b/cmd/ngctl/main.go @@ -0,0 +1,46 @@ +/* +Copyright 2021 Vesoft Inc. + +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 main + +import ( + "flag" + "fmt" + "os" + + "github.com/spf13/pflag" + "k8s.io/client-go/kubernetes/scheme" + + appsv1alpha1 "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd" +) + +func init() { + _ = appsv1alpha1.AddToScheme(scheme.Scheme) +} + +func main() { + flags := pflag.NewFlagSet("ngctl", pflag.ExitOnError) + _ = flag.CommandLine.Parse([]string{}) + pflag.CommandLine = flags + + command := cmd.NewNkctlCmd(os.Stdin, os.Stdout, os.Stderr) + + if err := command.Execute(); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index a75bed31..6e8dd45b 100644 --- a/go.mod +++ b/go.mod @@ -11,16 +11,20 @@ require ( github.com/onsi/gomega v1.10.2 github.com/openkruise/kruise-api v0.8.0 github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 github.com/vesoft-inc/nebula-go v0.0.0-20210322063004-e37085b865d6 go.uber.org/zap v1.15.0 + gopkg.in/yaml.v2 v2.3.0 k8s.io/api v0.19.2 k8s.io/apimachinery v0.19.2 k8s.io/apiserver v0.19.2 + k8s.io/cli-runtime v0.19.2 k8s.io/client-go v0.19.2 k8s.io/code-generator v0.19.2 - k8s.io/klog/v2 v2.4.0 + k8s.io/klog/v2 v2.4.0 // indirect k8s.io/kube-scheduler v0.0.0 + k8s.io/kubectl v0.0.0 k8s.io/kubernetes v1.19.2 k8s.io/utils v0.0.0-20200912215256-4140de9c8800 sigs.k8s.io/controller-runtime v0.7.0 diff --git a/go.sum b/go.sum index ae06bc27..c8976eeb 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,7 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v43.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0= @@ -39,6 +40,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20200415212048-7901bc822317/go.mod h1:DF8FZRxMHMGv/vP2lQP6h+dYzzjpuRn24VeRiYn3qjQ= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.10-0.20200715222032-5eafd1556990/go.mod h1:ay/0dTb7NsG8QMDfsRfLHgZo/6xAJShLe1+ePPflihk= @@ -82,6 +84,7 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= @@ -163,9 +166,11 @@ github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdR github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/facebook/fbthrift v0.0.0-20190922225929-2f9839604e25 h1:dezRDs9oGYxeavyvcNg/Js+dK6kIvfzERoJ7K8Xkv14= github.com/facebook/fbthrift v0.0.0-20190922225929-2f9839604e25/go.mod h1:2tncLx5rmw69e5kMBv/yJneERbzrr1yr5fdlnTbu8lU= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -174,6 +179,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -317,6 +323,7 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= @@ -340,6 +347,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/ishidawataru/sctp v0.0.0-20190723014705-7c296d48a2b5/go.mod h1:DM4VvS+hD/kDi1U1QsX2fnZowwBhqD0Dk3bRPKF/Oc8= github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= @@ -378,6 +386,7 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA= @@ -409,10 +418,12 @@ github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nr github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/ipvs v1.0.1/go.mod h1:2pngiyseZbIKXNv7hsKj3O9UEz30c53MT9005gt2hxQ= github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -464,6 +475,7 @@ github.com/openkruise/kruise-api v0.8.0 h1:bjkApJhzqLuuqxvMZ8rNKDy365ebg0iJonMFk github.com/openkruise/kruise-api v0.8.0/go.mod h1:nCf5vVOjQJX5OaV7Qi0Z51/Rn9cd7s5kVrg8YLgFp1I= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -501,6 +513,7 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -522,6 +535,7 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -566,6 +580,7 @@ github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe/go.mod h1:DD4vA1 github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -851,6 +866,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -867,6 +883,7 @@ k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc= k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apiserver v0.19.2 h1:xq2dXAzsAoHv7S4Xc/p7PKhiowdHV/PgdePWo3MxIYM= k8s.io/apiserver v0.19.2/go.mod h1:FreAq0bJ2vtZFj9Ago/X0oNGC51GfubKK/ViOKfVAOA= +k8s.io/cli-runtime v0.19.2 h1:d4uOtKhy3ImdaKqZJ8yQgLrdtUwsJLfP4Dw7L/kVPOo= k8s.io/cli-runtime v0.19.2/go.mod h1:CMynmJM4Yf02TlkbhKxoSzi4Zf518PukJ5xep/NaNeY= k8s.io/client-go v0.19.2 h1:gMJuU3xJZs86L1oQ99R4EViAADUPMHHtS9jFshasHSc= k8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA= @@ -918,10 +935,12 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9 h1:rusRLrDhjBp6aY sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQbTRyDlZPJX2SUPEqvnB+j7AJjtlox7PEwigU0= sigs.k8s.io/controller-runtime v0.7.0 h1:bU20IBBEPccWz5+zXpLnpVsgBYxqclaHu1pVDl/gEt8= sigs.k8s.io/controller-runtime v0.7.0/go.mod h1:pJ3YBrJiAqMAZKi6UVGuE98ZrroV1p+pIhoHsMm9wdU= +sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc h1:MksmcCZQWAQJCTA5T0jgI/0sJ51AVm4Z41MrmfczEoc= vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/pkg/ngctl/cmd/cmd.go b/pkg/ngctl/cmd/cmd.go new file mode 100644 index 00000000..4b731ec5 --- /dev/null +++ b/pkg/ngctl/cmd/cmd.go @@ -0,0 +1,89 @@ +/* +Copyright 2021 Vesoft Inc. + +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 cmd + +import ( + "flag" + "io" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/console" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/info" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/list" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/options" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/use" + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/version" +) + +const ( + nkcLongDescription = ` + "ngctl"(NebulaGraph Kubernetes Control) controls the Nebula cluster manager on Kubernetes. +` +) + +func NewNkctlCmd(in io.Reader, out, err io.Writer) *cobra.Command { + cmds := &cobra.Command{ + Use: "ngctl", + Short: "NebulaGraph kubernetes control.", + Long: nkcLongDescription, + Run: runHelp, + } + + flags := cmds.PersistentFlags() + + kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() + kubeConfigFlags.AddFlags(flags) + matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) + matchVersionKubeConfigFlags.AddFlags(cmds.PersistentFlags()) + + cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine) + + f := cmdutil.NewFactory(matchVersionKubeConfigFlags) + + ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err} + + groups := templates.CommandGroups{ + { + Message: "Cluster Management Commands:", + Commands: []*cobra.Command{ + console.NewCmdConsole(f, ioStreams), + info.NewCmdInfo(f, ioStreams), + list.NewCmdList(f, ioStreams), + use.NewCmdUse(f, ioStreams), + version.NewCmdVersion(f, ioStreams), + }, + }, + } + + groups.Add(cmds) + + filters := []string{"options"} + + templates.ActsAsRootCommand(cmds, filters, groups...) + + cmds.AddCommand(options.NewCmdOptions(ioStreams.Out)) + + return cmds +} + +func runHelp(cmd *cobra.Command, _ []string) { + cmdutil.CheckErr(cmd.Help()) +} diff --git a/pkg/ngctl/cmd/console/console.go b/pkg/ngctl/cmd/console/console.go new file mode 100644 index 00000000..3874a226 --- /dev/null +++ b/pkg/ngctl/cmd/console/console.go @@ -0,0 +1,216 @@ +/* +Copyright 2021 Vesoft Inc. + +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 console + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" + "github.com/vesoft-inc/nebula-operator/pkg/label" + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/executor" +) + +const ( + consoleLong = ` + Open console to the specified nebula cluster. +` + consoleExample = ` + # Open console to the current nebula cluster, which is set by 'ngctl use' command + ngctl console + + # Open console to the specified nebula cluster + ngctl console CLUSTER_NAME +` + consoleUsage = `expected 'console CLUSTER_NAME' for the console command, or using 'ngctl use' +to set nebula cluster first. +` + consolePodGenerateNameFmt = "console-%s-" + consoleContainerName = "console" + consoleDefaultImage = "vesoft/nebula-console:v2-nightly" +) + +type ( + // Options is a struct to support version command + Options struct { + Namespace string + NebulaClusterName string + Image string + ImagePullPolicy corev1.PullPolicy + + User string + Password string + + runtimeCli client.Client + restClientGetter genericclioptions.RESTClientGetter + genericclioptions.IOStreams + } +) + +// NewOptions returns initialized Options +func NewOptions(ioStreams genericclioptions.IOStreams) *Options { + return &Options{ + IOStreams: ioStreams, + Image: consoleDefaultImage, + User: "root", + Password: "*", + } +} + +// NewCmdConsole returns a cobra command for specify a nebula cluster to use +func NewCmdConsole(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + cmd := &cobra.Command{ + Use: "console", + Short: "Open console to the nebula cluster", + Long: consoleLong, + Example: consoleExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate(cmd)) + cmdutil.CheckErr(o.Run()) + }, + } + + f.AddFlags(cmd) + o.AddFlags(cmd) + + return cmd +} + +// AddFlags add all the flags. +func (o *Options) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Image, "image", o.Image, "Container image to use for open console to the nebula cluster.") + cmd.Flags().String("image-pull-policy", "", + "The image pull policy for the container."+ + " If left empty, this value will not be specified by the client and defaulted by the server.") + cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The Nebula Graph login user name.") + cmd.Flags().StringVarP(&o.Password, "password", "p", o.Password, "The Nebula Graph login password.") +} + +// Complete completes all the required options +func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + o.NebulaClusterName, o.Namespace, err = f.GetNebulaClusterNameAndNamespace(true, args) + if err != nil && !cmdutil.IsErNotSpecified(err) { + return err + } + + var imagePullPolicy string + imagePullPolicy, err = cmd.Flags().GetString("image-pull-policy") + if err != nil { + return err + } + + o.ImagePullPolicy = corev1.PullPolicy(imagePullPolicy) + + o.runtimeCli, err = f.ToRuntimeClient() + if err != nil { + return err + } + + o.restClientGetter = f + + return nil +} + +// Validate validates the provided options +func (o *Options) Validate(cmd *cobra.Command) error { + if o.NebulaClusterName == "" { + return cmdutil.UsageErrorf(cmd, consoleUsage) + } + + switch o.ImagePullPolicy { + case corev1.PullAlways, corev1.PullIfNotPresent, corev1.PullNever, "": + default: + return fmt.Errorf("invalid image pull policy: %s", o.ImagePullPolicy) + } + + if o.User == "" { + return fmt.Errorf("the Nebula Graph login user name cannot be empty") + } + + if o.Password == "" { + return fmt.Errorf("the Nebula Graph login password cannot be empty") + } + + return nil +} + +// Run executes use command +func (o *Options) Run() error { + pod, err := o.generateConsolePod() + if err != nil { + return err + } + + e := executor.NewPodExecutor(pod, consoleContainerName, o.restClientGetter, o.IOStreams) + return e.Execute(context.TODO()) +} + +func (o *Options) generateConsolePod() (*corev1.Pod, error) { + var nc appsv1alpha1.NebulaCluster + key := client.ObjectKey{Namespace: o.Namespace, Name: o.NebulaClusterName} + if err := o.runtimeCli.Get(context.TODO(), key, &nc); err != nil { + return nil, err + } + + consoleContainer := corev1.Container{ + Name: consoleContainerName, + Image: o.Image, + Command: []string{ + "nebula-console", + "-u", o.User, + "-p", o.Password, + "--addr", nc.GraphdComponent().GetServiceName(), + "--port", strconv.Itoa(int(nc.GraphdComponent().GetPort(appsv1alpha1.GraphdPortNameThrift))), + }, + Stdin: true, + StdinOnce: true, + TTY: true, + } + + if o.ImagePullPolicy != "" { + consoleContainer.ImagePullPolicy = o.ImagePullPolicy + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf(consolePodGenerateNameFmt, o.NebulaClusterName), + Namespace: o.Namespace, + Labels: map[string]string{ + label.ClusterLabelKey: nc.GetClusterName(), + label.ComponentLabelKey: "console", + "app.kubernetes.io/managed-by": "ngctl", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{consoleContainer}, + RestartPolicy: corev1.RestartPolicyNever, + }, + } + + return pod, nil +} diff --git a/pkg/ngctl/cmd/info/clusterinfo.go b/pkg/ngctl/cmd/info/clusterinfo.go new file mode 100644 index 00000000..c6d8b82a --- /dev/null +++ b/pkg/ngctl/cmd/info/clusterinfo.go @@ -0,0 +1,256 @@ +/* +Copyright 2021 Vesoft Inc. + +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 info + +import ( + "bytes" + "context" + "fmt" + "text/tabwriter" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" + "github.com/vesoft-inc/nebula-operator/pkg/label" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util/ignore" +) + +type ( + NebulaClusterInfo struct { + Name string + Namespace string + CreationTimestamp time.Time + Components []NebulaComponentInfo + Service NebulaServiceInfo + } + + NebulaComponentInfo struct { + Type string + Phase string + Replicas int32 + ReadyReplicas int32 + CPU resource.Quantity + Memory resource.Quantity + Storage resource.Quantity + Image string + } + + NebulaServiceInfo struct { + Type corev1.ServiceType + Port int32 + Endpoints []*NebulaServiceEndpoints + } + + NebulaServiceEndpoints struct { + Desc string + Endpoints []string + } +) + +func NewNebulaClusterInfo(clusterName, namespace string, runtimeCli client.Client) (*NebulaClusterInfo, error) { + var nc appsv1alpha1.NebulaCluster + key := client.ObjectKey{Namespace: namespace, Name: clusterName} + if err := runtimeCli.Get(context.TODO(), key, &nc); err != nil { + return nil, err + } + + var svc corev1.Service + key = client.ObjectKey{Namespace: namespace, Name: nc.GraphdComponent().GetServiceName()} + if err := runtimeCli.Get(context.TODO(), key, &svc); err != nil { + return nil, err + } + + selector, err := label.New().Cluster(clusterName).Graphd().Selector() + if err != nil { + return nil, err + } + var pods corev1.PodList + listOptions := client.ListOptions{ + LabelSelector: selector, + Namespace: namespace, + } + if err := runtimeCli.List(context.TODO(), &pods, &listOptions); err != nil { + return nil, err + } + + graphd := NebulaComponentInfo{ + Phase: string(nc.Status.Graphd.Phase), + Replicas: nc.Status.Graphd.Workload.Replicas, + ReadyReplicas: nc.Status.Graphd.Workload.ReadyReplicas, + Image: nc.GraphdComponent().GetImage(), + } + setComponentInfo(&graphd, nc.GraphdComponent()) + + metad := NebulaComponentInfo{ + Phase: string(nc.Status.Metad.Phase), + Replicas: nc.Status.Metad.Workload.Replicas, + ReadyReplicas: nc.Status.Metad.Workload.ReadyReplicas, + } + setComponentInfo(&metad, nc.MetadComponent()) + + storage := NebulaComponentInfo{ + Phase: string(nc.Status.Storaged.Phase), + Replicas: nc.Status.Storaged.Workload.Replicas, + ReadyReplicas: nc.Status.Storaged.Workload.ReadyReplicas, + } + setComponentInfo(&storage, nc.StoragedComponent()) + + nci := &NebulaClusterInfo{ + Name: clusterName, + Namespace: namespace, + CreationTimestamp: nc.CreationTimestamp.Time, + Components: []NebulaComponentInfo{graphd, metad, storage}, + Service: NebulaServiceInfo{ + Type: svc.Spec.Type, + Port: nc.GraphdComponent().GetPort(appsv1alpha1.GraphdPortNameThrift), + Endpoints: nil, + }, + } + + nci.appendEndpointsLoadBalancer(&svc) + nci.appendEndpointsNodePort(&svc, &pods) + nci.appendEndpointsService(&svc) + nci.appendEndpointsClusterIP(&svc) + + return nci, nil +} + +func (i *NebulaClusterInfo) Render() (string, error) { + tw := new(tabwriter.Writer) + buf := &bytes.Buffer{} + tw.Init(buf, 0, 8, 2, ' ', 0) + + ignore.Fprintf(tw, "Name:\t%s\n", i.Name) + ignore.Fprintf(tw, "Namespace:\t%s\n", i.Namespace) + ignore.Fprintf(tw, "CreationTimestamp:\t%s\n", i.CreationTimestamp) + ignore.Fprintf(tw, "Overview:\n") + ignore.Fprintf(tw, " \tPhase\tReady\tCPU\tMemory\tStorage\tVersion\n") + ignore.Fprintf(tw, " \t-----\t-----\t---\t------\t-------\t-------\n") + + for ix := range i.Components { + c := &i.Components[ix] + ignore.Fprintf(tw, " %s:\t", c.Type) + ignore.Fprintf(tw, "%s\t%d/%d\t%s\t%s\t%s\t%s\t\n", + c.Phase, c.ReadyReplicas, c.Replicas, c.CPU.String(), c.Memory.String(), c.Storage.String(), c.Image) + } + + ignore.Fprintf(tw, "Service(%s):\n", i.Service.Type) + for _, sp := range i.Service.Endpoints { + ignore.Fprintf(tw, " %s\n", sp.Desc) + for _, ep := range sp.Endpoints { + ignore.Fprintf(tw, " - %s\n", ep) + } + } + + _ = tw.Flush() + return buf.String(), nil +} + +func (i *NebulaClusterInfo) appendEndpointsLoadBalancer(svc *corev1.Service) { + if svc.Spec.Type != corev1.ServiceTypeLoadBalancer { + return + } + nse := NebulaServiceEndpoints{ + Desc: "Outer kubernetes: LoadBalance", + } + i.Service.Endpoints = append(i.Service.Endpoints, &nse) + + for _, ingress := range svc.Status.LoadBalancer.Ingress { + if ingress.Hostname != "" { + nse.Endpoints = []string{fmt.Sprintf("%s:%d", ingress.Hostname, i.Service.Port)} + } + if ingress.IP != "" { + nse.Endpoints = []string{fmt.Sprintf("%s:%d", ingress.IP, i.Service.Port)} + } + } + + if len(nse.Endpoints) == 0 { + nse.Endpoints = []string{"Pending"} + } +} + +func (i *NebulaClusterInfo) appendEndpointsNodePort(svc *corev1.Service, pods *corev1.PodList) { + if svc.Spec.Type != corev1.ServiceTypeNodePort { + return + } + + getNodePort := func() int32 { + for _, p := range svc.Spec.Ports { + if p.Name == appsv1alpha1.GraphdPortNameThrift { + return p.NodePort + } + } + return 0 + } + nse := NebulaServiceEndpoints{ + Desc: "Outer kubernetes: NodePort", + } + i.Service.Endpoints = append(i.Service.Endpoints, &nse) + nodePort := getNodePort() + if nodePort == 0 { + nse.Endpoints = []string{"No suitable port"} + return + } + + for ix := range pods.Items { + pod := &pods.Items[ix] + if pod.Status.Phase == corev1.PodRunning { + nse.Endpoints = append(nse.Endpoints, fmt.Sprintf("%s:%d", pod.Status.HostIP, nodePort)) + } + } + if len(nse.Endpoints) == 0 { + nse.Endpoints = []string{"No running pod"} + } +} + +func (i *NebulaClusterInfo) appendEndpointsService(svc *corev1.Service) { + i.Service.Endpoints = append(i.Service.Endpoints, &NebulaServiceEndpoints{ + Desc: "Inter kubernetes: Service", + Endpoints: []string{fmt.Sprintf("%s.%s.svc:%d", svc.Name, svc.Namespace, i.Service.Port)}, + }) +} + +func (i *NebulaClusterInfo) appendEndpointsClusterIP(svc *corev1.Service) { + if svc.Spec.ClusterIP == "" { + return + } + i.Service.Endpoints = append(i.Service.Endpoints, &NebulaServiceEndpoints{ + Desc: "Inter kubernetes: ClusterIP", + Endpoints: []string{fmt.Sprintf("%s:%d", svc.Spec.ClusterIP, i.Service.Port)}, + }) +} + +func setComponentInfo(info *NebulaComponentInfo, c appsv1alpha1.NebulaClusterComponentter) { + info.Type = string(c.Type()) + if res := c.GetResources(); res != nil { + if cpu := res.Requests.Cpu(); cpu != nil { + info.CPU = *cpu + } + if memory := res.Requests.Memory(); memory != nil { + info.Memory = *memory + } + } + if res := c.GetStorageResources(); res != nil { + if storage := res.Requests.Storage(); storage != nil { + info.Storage = *storage + } + } + info.Image = c.GetImage() +} diff --git a/pkg/ngctl/cmd/info/info.go b/pkg/ngctl/cmd/info/info.go new file mode 100644 index 00000000..5dbbfb78 --- /dev/null +++ b/pkg/ngctl/cmd/info/info.go @@ -0,0 +1,119 @@ +/* +Copyright 2021 Vesoft Inc. + +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 info + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" + + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" +) + +const ( + infoLong = ` + Get specified nebula cluster information. +` + infoExample = ` + # get current nebula cluster information, which is set by 'ngctl use' command + ngctl info + + # get specified nebula cluster information + ngctl info CLUSTER_NAME +` + infoUsage = `expected 'info CLUSTER_NAME' for the info command, or using 'ngctl use' +to set nebula cluster first. +` +) + +type ( + // Options is a struct to support version command + Options struct { + Namespace string + NebulaClusterName string + + runtimeCli client.Client + genericclioptions.IOStreams + } +) + +// NewOptions returns initialized Options +func NewOptions(streams genericclioptions.IOStreams) *Options { + return &Options{ + IOStreams: streams, + } +} + +// NewCmdInfo returns a cobra command for specify a nebula cluster to use +func NewCmdInfo(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + cmd := &cobra.Command{ + Use: "info", + Short: "Show nebula cluster information", + Long: infoLong, + Example: infoExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, args)) + cmdutil.CheckErr(o.Validate(cmd)) + cmdutil.CheckErr(o.Run()) + }, + } + + f.AddFlags(cmd) + + return cmd +} + +// Complete completes all the required options +func (o *Options) Complete(f cmdutil.Factory, args []string) error { + var err error + o.NebulaClusterName, o.Namespace, err = f.GetNebulaClusterNameAndNamespace(true, args) + if err != nil && !cmdutil.IsErNotSpecified(err) { + return err + } + + o.runtimeCli, err = f.ToRuntimeClient() + if err != nil { + return err + } + + return nil +} + +// Validate validates the provided options +func (o *Options) Validate(cmd *cobra.Command) error { + if o.NebulaClusterName == "" { + return cmdutil.UsageErrorf(cmd, infoUsage) + } + return nil +} + +// Run executes use command +func (o *Options) Run() error { + nci, err := NewNebulaClusterInfo(o.NebulaClusterName, o.Namespace, o.runtimeCli) + if err != nil { + return err + } + str, err := nci.Render() + if err != nil { + return err + } + _, err = fmt.Fprint(o.Out, str) + return err +} diff --git a/pkg/ngctl/cmd/list/alias.go b/pkg/ngctl/cmd/list/alias.go new file mode 100644 index 00000000..334afc10 --- /dev/null +++ b/pkg/ngctl/cmd/list/alias.go @@ -0,0 +1,33 @@ +/* +Copyright 2021 Vesoft Inc. + +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 list + +import ( + kubectlget "k8s.io/kubectl/pkg/cmd/get" +) + +type ( + PrintFlags = kubectlget.PrintFlags + SortingPrinter = kubectlget.SortingPrinter + OriginalPositioner = kubectlget.OriginalPositioner + TablePrinter = kubectlget.TablePrinter +) + +var ( + NewGetPrintFlags = kubectlget.NewGetPrintFlags + NewRuntimeSorter = kubectlget.NewRuntimeSorter +) diff --git a/pkg/ngctl/cmd/list/list.go b/pkg/ngctl/cmd/list/list.go new file mode 100644 index 00000000..508f1e3d --- /dev/null +++ b/pkg/ngctl/cmd/list/list.go @@ -0,0 +1,393 @@ +/* +Copyright 2021 Vesoft Inc. + +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 list + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" +) + +const ( + listLong = ` + List nebula clusters. + + Prints a table of the most important information about the nebula clusters. You can + filter the list using a label selector and the --selector flag. You will only see + results in your current namespace unless you pass --all-namespaces. + + By specifying the output as 'template' and providing a Go template as the value of + the --template flag, you can filter the attributes of the nebula clusters. +` + listExample = ` + # List all nebula clusters. + ngctl list + + # List all nebula clusters in all namespaces. + ngctl list -A + + # List all nebula clusters with json format. + ngctl list -o json + + # List a single nebula clusters with specified NAME in ps output format. + ngctl list NAME + + # Return only the metad's phase value of the specified pod. + ngctl list -o template --template="{{.status.graphd.phase}}" NAME + + # List image information in custom columns. + ngctl list -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.graphd.image +` + OutputFormatWide = "wide" +) + +type ( + // Options is a struct to support version command + Options struct { + LabelSelector string + FieldSelector string + AllNamespaces bool + Namespace string + NebulaClusterNames []string + Sort bool + SortBy string + IgnoreNotFound bool + IsHumanReadablePrinter bool + ServerPrint bool + + PrintFlags *PrintFlags + ToPrinter func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) + + builder *resource.Builder + genericclioptions.IOStreams + } +) + +// NewOptions returns initialized Options +func NewOptions(streams genericclioptions.IOStreams) *Options { + return &Options{ + PrintFlags: NewGetPrintFlags(), + IOStreams: streams, + ServerPrint: true, + } +} + +// NewCmdList returns a cobra command for list nebula clusters +func NewCmdList(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + cmd := &cobra.Command{ + Use: "list", + Short: "list all nebula clusters", + Long: listLong, + Example: listExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Run()) + }, + } + + o.PrintFlags.AddFlags(cmd) + o.AddFlags(cmd) + + return cmd +} + +// AddFlags add all the flags. +func (o *Options) AddFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, + "If the requested object does not exist the command will return exit code 0.") + cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, + "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") + cmd.Flags().StringVar(&o.FieldSelector, "field-selector", o.FieldSelector, + "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2)."+ + "The server only supports a limited number of field queries per type.") + cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, + "If present, list the nebula clusters across all namespaces.") + cmd.Flags().BoolVar(&o.ServerPrint, "server-print", o.ServerPrint, + "If true, have the server return the appropriate table output. Supports extension APIs and CRDs.") +} + +// Complete completes all the required options +func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + o.NebulaClusterNames, o.Namespace, err = f.GetNebulaClusterNamesAndNamespace(false, args) + if err != nil && !cmdutil.IsErNotSpecified(err) { + return err + } + + o.SortBy, err = cmd.Flags().GetString("sort-by") + if err != nil { + return err + } + o.Sort = o.SortBy != "" + + outputOption := cmd.Flags().Lookup("output").Value.String() + if outputOption == "custom-columns" { + o.ServerPrint = false + } + + templateArg := "" + if o.PrintFlags.TemplateFlags != nil && o.PrintFlags.TemplateFlags.TemplateArgument != nil { + templateArg = *o.PrintFlags.TemplateFlags.TemplateArgument + } + + if (*o.PrintFlags.OutputFormat == "" && templateArg == "") || *o.PrintFlags.OutputFormat == OutputFormatWide { + o.IsHumanReadablePrinter = true + } + + o.ToPrinter = func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) { + printFlags := o.PrintFlags.Copy() + + if mapping != nil { + printFlags.SetKind(mapping.GroupVersionKind.GroupKind()) + } + if withNamespace { + if err := printFlags.EnsureWithNamespace(); err != nil { + return nil, err + } + } + + printer, err := printFlags.ToPrinter() + if err != nil { + return nil, err + } + printer, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(printer, nil) + if err != nil { + return nil, err + } + + if o.Sort { + printer = &SortingPrinter{Delegate: printer, SortField: o.SortBy} + } + + if o.ServerPrint { + printer = &TablePrinter{Delegate: printer} + } + + return printer.PrintObj, nil + } + + o.builder = f.NewBuilder() + + return nil +} + +func (o *Options) Validate(cmd *cobra.Command) error { + if showLabels, err := cmd.Flags().GetBool("show-labels"); err != nil { + return err + } else if showLabels { + outputOption := cmd.Flags().Lookup("output").Value.String() + if outputOption != "" && outputOption != OutputFormatWide { + return fmt.Errorf("--show-labels option cannot be used with %s printer", outputOption) + } + } + + return nil +} + +func (o *Options) transformRequests(req *rest.Request) { + if !o.ServerPrint || !o.IsHumanReadablePrinter { + return + } + + req.SetHeader("Accept", strings.Join([]string{ + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), + "application/json", + }, ",")) + + if o.Sort { + req.Param("includeObject", "Object") + } +} + +// Run executes use command +func (o *Options) Run() error { + r := o.builder. + Unstructured(). + NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). + LabelSelectorParam(o.LabelSelector). + FieldSelectorParam(o.FieldSelector). + ResourceTypeOrNameArgs(true, append([]string{"nebulacluster"}, o.NebulaClusterNames...)...). + ContinueOnError(). + Latest(). + Flatten(). + TransformRequests(o.transformRequests). + Do() + + if o.IgnoreNotFound { + r.IgnoreErrors(apierrors.IsNotFound) + } + if err := r.Err(); err != nil { + return err + } + + if !o.IsHumanReadablePrinter { + return o.printGeneric(r) + } + + infos, err := r.Infos() + if err != nil { + return err + } + + objs := make([]runtime.Object, len(infos)) + for ix := range infos { + objs[ix] = infos[ix].Object + } + + var positioner OriginalPositioner + if o.Sort { + sorter := NewRuntimeSorter(objs, o.SortBy) + if err := sorter.Sort(); err != nil { + return err + } + positioner = sorter + } + + w := printers.GetNewTabWriter(o.Out) + defer func() { + _ = w.Flush() + }() + + var printer printers.ResourcePrinter + for ix := range objs { + var mapping *meta.RESTMapping + var info *resource.Info + + if positioner != nil { + info = infos[positioner.OriginalPosition(ix)] + mapping = info.Mapping + } else { + info = infos[ix] + mapping = info.Mapping + } + + printWithNamespace := o.AllNamespaces + if mapping != nil && mapping.Scope.Name() == meta.RESTScopeNameRoot { + printWithNamespace = false + } + + if printer == nil { + printer, err = o.ToPrinter(mapping, printWithNamespace) + if err != nil { + return err + } + } + + if err := printer.PrintObj(info.Object, w); err != nil { + return err + } + } + + return nil +} + +func (o *Options) printGeneric(r *resource.Result) error { + var errs []error + singleItemImplied := false + infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos() + if err != nil { + if singleItemImplied { + return err + } + errs = append(errs, err) + } + + if len(infos) == 0 && o.IgnoreNotFound { + return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs))) + } + + printer, err := o.ToPrinter(nil, false) + if err != nil { + return err + } + + var obj runtime.Object + if !singleItemImplied || len(infos) != 1 { + list := corev1.List{ + TypeMeta: metav1.TypeMeta{ + Kind: "List", + APIVersion: "v1", + }, + ListMeta: metav1.ListMeta{}, + } + + for _, info := range infos { + list.Items = append(list.Items, runtime.RawExtension{Object: info.Object}) + } + + listData, err := json.Marshal(list) + if err != nil { + return err + } + + converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, listData) + if err != nil { + return err + } + + obj = converted + } else { + obj = infos[0].Object + } + + if !meta.IsListType(obj) { + return printer.PrintObj(obj, o.Out) + } + + items, err := meta.ExtractList(obj) + if err != nil { + return err + } + + list := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "kind": "List", + "apiVersion": "v1", + "metadata": map[string]interface{}{}, + }, + } + if listMeta, err := meta.ListAccessor(obj); err == nil { + list.Object["metadata"] = map[string]interface{}{ + "selfLink": listMeta.GetSelfLink(), + "resourceVersion": listMeta.GetResourceVersion(), + } + } + + for _, item := range items { + list.Items = append(list.Items, *item.(*unstructured.Unstructured)) + } + return printer.PrintObj(list, o.Out) +} diff --git a/pkg/ngctl/cmd/options/options.go b/pkg/ngctl/cmd/options/options.go new file mode 100644 index 00000000..732614c2 --- /dev/null +++ b/pkg/ngctl/cmd/options/options.go @@ -0,0 +1,48 @@ +/* +Copyright 2021 Vesoft Inc. + +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 options + +import ( + "io" + + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/util/templates" + + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" +) + +var optionsExample = templates.Examples(` + # Print flags inherited by all commands + ngctl options`) + +// NewCmdOptions implements the options command +func NewCmdOptions(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "options", + Short: "Print the list of flags inherited by all commands", + Long: "Print the list of flags inherited by all commands", + Example: optionsExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(cmd.Usage()) + }, + } + + cmd.SetOut(out) + + templates.UseOptionsTemplates(cmd) + return cmd +} diff --git a/pkg/ngctl/cmd/use/use.go b/pkg/ngctl/cmd/use/use.go new file mode 100644 index 00000000..a3ae9a4b --- /dev/null +++ b/pkg/ngctl/cmd/use/use.go @@ -0,0 +1,130 @@ +/* +Copyright 2021 Vesoft Inc. + +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 use + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/config" +) + +const ( + useLong = ` + Specify a nebula cluster to use. + + By using a certain cluster, you may omit --nebulacluster option + in many control commands. +` + useExample = ` + # specify a nebula cluster to use + ngctl use demo-cluster + # specify kubernetes context and namespace + ngctl use --namespace=demo-ns demo-cluster +` + useUsage = "expected 'use CLUSTER_NAME' for the use command" +) + +type ( + // Options is a struct to support version command + Options struct { + Namespace string + NebulaClusterName string + NebulaClusterConfigFile string + + runtimeCli client.Client + genericclioptions.IOStreams + } +) + +// NewOptions returns initialized Options +func NewOptions(streams genericclioptions.IOStreams) *Options { + return &Options{ + IOStreams: streams, + } +} + +// NewCmdUse returns a cobra command for specify a nebula cluster to use +func NewCmdUse(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + cmd := &cobra.Command{ + Use: "use", + Short: "specify a nebula cluster to use", + Long: useLong, + Example: useExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, args)) + cmdutil.CheckErr(o.Validate(cmd)) + cmdutil.CheckErr(o.Run()) + }, + } + + f.AddFlags(cmd) + + return cmd +} + +// Complete completes all the required options +func (o *Options) Complete(f cmdutil.Factory, args []string) error { + var err error + o.NebulaClusterName, o.Namespace, err = f.GetNebulaClusterNameAndNamespace(false, args) + if err != nil && !cmdutil.IsErNotSpecified(err) { + return err + } + + o.runtimeCli, err = f.ToRuntimeClient() + if err != nil { + return err + } + + o.NebulaClusterConfigFile, err = f.GetNebulaClusterConfigFile() + if err != nil { + return err + } + + return nil +} + +// Validate validates the provided options +func (o *Options) Validate(cmd *cobra.Command) error { + if o.NebulaClusterName == "" { + return cmdutil.UsageErrorf(cmd, useUsage) + } + return nil +} + +// Run executes use command +func (o *Options) Run() error { + var nc appsv1alpha1.NebulaCluster + key := client.ObjectKey{Namespace: o.Namespace, Name: o.NebulaClusterName} + if err := o.runtimeCli.Get(context.TODO(), key, &nc); err != nil { + return err + } + + if err := config.NewNebulaClusterConfig(nc.Namespace, nc.Name).SaveToFile(o.NebulaClusterConfigFile); err != nil { + return err + } + _, err := fmt.Fprintf(o.Out, "Nebula cluster config switch to %s/%s, save to %q.\n", + nc.Namespace, nc.Name, o.NebulaClusterConfigFile) + return err +} diff --git a/pkg/ngctl/cmd/util/alias.go b/pkg/ngctl/cmd/util/alias.go new file mode 100644 index 00000000..34f4b2b7 --- /dev/null +++ b/pkg/ngctl/cmd/util/alias.go @@ -0,0 +1,27 @@ +/* +Copyright 2021 Vesoft Inc. + +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 util + +import ( + kcmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +var ( + NewMatchVersionFlags = kcmdutil.NewMatchVersionFlags + CheckErr = kcmdutil.CheckErr + UsageErrorf = kcmdutil.UsageErrorf +) diff --git a/pkg/ngctl/cmd/util/factory.go b/pkg/ngctl/cmd/util/factory.go new file mode 100644 index 00000000..7a173ed5 --- /dev/null +++ b/pkg/ngctl/cmd/util/factory.go @@ -0,0 +1,187 @@ +/* +Copyright 2021 Vesoft Inc. + +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 util + +import ( + "sync" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + kcmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/config" +) + +type ( + Factory interface { + kcmdutil.Factory + + AddFlags(cmd *cobra.Command) + ToRuntimeClient() (client.Client, error) + GetNebulaClusterNameAndNamespace(withUseConfig bool, args []string) (string, string, error) + GetNebulaClusterNamesAndNamespace(withUseConfig bool, args []string) ([]string, string, error) + // GetNebulaClusterName() (string, error) + // GetNebulaClusterNameWithoutConfig() string + // GetNamespace() (string, error) + GetNebulaClusterConfigFile() (string, error) + } + factoryImpl struct { + kcmdutil.Factory + + nebulaClusterName string + nebulaClusterConfigFile string + + loadingLock sync.Mutex + nebulaClusterConfig *config.NebulaClusterConfig + } +) + +var ( + _ Factory = (*factoryImpl)(nil) + errNotSpecified = errors.New("Not Specified") +) + +func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory { + return &factoryImpl{ + Factory: kcmdutil.NewFactory(clientGetter), + } +} + +func IsErNotSpecified(err error) bool { + return err == errNotSpecified +} + +func (f *factoryImpl) AddFlags(cmd *cobra.Command) { + location, _ := config.DefaultConfigLocation() + cmd.Flags().StringVar(&f.nebulaClusterName, "nebulacluster", "", "Specify the nebula cluster.") + cmd.Flags().StringVar(&f.nebulaClusterConfigFile, "config", location, "Specify the nebula cluster config.") +} + +func (f *factoryImpl) ToRuntimeClient() (client.Client, error) { + restConfig, err := f.ToRESTConfig() + if err != nil { + return nil, err + } + + return client.New(restConfig, client.Options{}) +} + +func (f *factoryImpl) GetNamespace() (string, error) { + namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return "", err + } + + if enforceNamespace { + return namespace, err + } + + c, err := f.getNebulaClusterConfig() + if err != nil { + return namespace, nil + } + + return c.Namespace, err +} + +func (f *factoryImpl) GetNebulaClusterNameAndNamespace(withUseConfig bool, args []string) (name, namespace string, err error) { + var names []string + names, namespace, err = f.GetNebulaClusterNamesAndNamespace(withUseConfig, args) + if len(names) > 0 { + name = names[0] + } + return name, namespace, err +} + +func (f *factoryImpl) GetNebulaClusterNamesAndNamespace(withUseConfig bool, args []string) (names []string, namespace string, err error) { + var enforceNamespace bool + namespace, enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil, "", err + } + + if f.nebulaClusterName != "" { + return []string{f.nebulaClusterName}, namespace, nil + } + + if len(args) > 0 { + return args, namespace, nil + } + + if enforceNamespace || !withUseConfig { + return nil, namespace, errNotSpecified + } + + c, err := f.getNebulaClusterConfig() + if err != nil { + return nil, "", errNotSpecified + } + + return []string{c.ClusterName}, c.Namespace, nil +} + +func (f *factoryImpl) GetNebulaClusterName() (string, error) { + return f.getNebulaClusterName(true) +} + +func (f *factoryImpl) GetNebulaClusterNameWithoutConfig() string { + name, _ := f.getNebulaClusterName(false) + return name +} + +func (f *factoryImpl) getNebulaClusterName(withConfig bool) (string, error) { + if !withConfig || f.nebulaClusterName != "" { + return f.nebulaClusterName, nil + } + + c, err := f.getNebulaClusterConfig() + if err != nil { + return "", err + } + + return c.ClusterName, nil +} + +func (f *factoryImpl) GetNebulaClusterConfigFile() (string, error) { + if f.nebulaClusterConfigFile != "" { + return f.nebulaClusterConfigFile, nil + } + return config.DefaultConfigLocation() +} + +func (f *factoryImpl) getNebulaClusterConfig() (*config.NebulaClusterConfig, error) { + if f.nebulaClusterConfig != nil { + return f.nebulaClusterConfig, nil + } + + f.loadingLock.Lock() + defer f.loadingLock.Unlock() + if f.nebulaClusterConfig != nil { + return f.nebulaClusterConfig, nil + } + + c := &config.NebulaClusterConfig{} + if err := c.LoadFromFile(f.nebulaClusterConfigFile); err != nil { + return nil, errors.Wrap(err, "unable to load nebula cluster config") + } + + f.nebulaClusterConfig = c + + return c, nil +} diff --git a/pkg/ngctl/cmd/util/ignore/io.go b/pkg/ngctl/cmd/util/ignore/io.go new file mode 100644 index 00000000..bb5c9807 --- /dev/null +++ b/pkg/ngctl/cmd/util/ignore/io.go @@ -0,0 +1,26 @@ +/* +Copyright 2021 Vesoft Inc. + +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 ignore + +import ( + "fmt" + "io" +) + +func Fprintf(w io.Writer, format string, a ...interface{}) { + _, _ = fmt.Fprintf(w, format, a...) +} diff --git a/pkg/ngctl/cmd/version/version.go b/pkg/ngctl/cmd/version/version.go new file mode 100644 index 00000000..166d9329 --- /dev/null +++ b/pkg/ngctl/cmd/version/version.go @@ -0,0 +1,123 @@ +/* +Copyright 2021 Vesoft Inc. + +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 ( + "context" + "fmt" + + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + "k8s.io/kubernetes/pkg/apis/core" + + "github.com/vesoft-inc/nebula-operator/pkg/label" + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util/ignore" + operatorversion "github.com/vesoft-inc/nebula-operator/pkg/version" +) + +const ( + versionExample = ` + # Print the cli and nebula operator version + ngctl version +` +) + +type ( + // Options is a struct to support version command + Options struct { + ClientOnly bool + + kubeCli kubernetes.Interface + genericclioptions.IOStreams + } +) + +// NewOptions returns initialized Options +func NewOptions(ioStreams genericclioptions.IOStreams) *Options { + return &Options{ + IOStreams: ioStreams, + } +} + +// NewCmdVersion returns a cobra command for fetching versions +func NewCmdVersion(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + cmd := &cobra.Command{ + Use: "version", + Short: "Print the cli and nebula operator version", + Example: versionExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f)) + cmdutil.CheckErr(o.Run()) + }, + } + + o.AddFlags(cmd) + + return cmd +} + +// AddFlags add all the flags. +func (o *Options) AddFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.ClientOnly, "client", o.ClientOnly, "If true, shows client version only (no server required).") +} + +// Complete completes all the required options +func (o *Options) Complete(f cmdutil.Factory) error { + var err error + if o.ClientOnly { + return nil + } + + o.kubeCli, err = f.KubernetesClientSet() + return err +} + +// Run executes version command +func (o *Options) Run() error { + clientVersion := operatorversion.Version() + + ignore.Fprintf(o.Out, "Client Version: %s\n", clientVersion) + + if o.ClientOnly { + return nil + } + + controllers, err := o.kubeCli.AppsV1().Deployments(core.NamespaceAll).List(context.TODO(), v1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s,%s=%s", label.NameLabelKey, "nebula-operator", label.ComponentLabelKey, "controller-manager"), + }) + if err != nil { + return err + } + + if len(controllers.Items) == 0 { + ignore.Fprintf(o.Out, "No Nebula Controller Manager found, please install first\n") + } else if len(controllers.Items) == 1 { + ignore.Fprintf(o.Out, "Nebula Controller Manager Version: %s\n", controllers.Items[0].Spec.Template.Spec.Containers[0].Image) + } else { + ignore.Fprintf(o.Out, "Nebula Controller Manager Versions:\n") + for i := range controllers.Items { + item := &controllers.Items[i] + ignore.Fprintf(o.Out, "\t%s/%s: %s\n", item.Namespace, item.Name, item.Spec.Template.Spec.Containers[0].Image) + } + } + + return nil +} diff --git a/pkg/ngctl/config/config.go b/pkg/ngctl/config/config.go new file mode 100644 index 00000000..a0e4004d --- /dev/null +++ b/pkg/ngctl/config/config.go @@ -0,0 +1,98 @@ +/* +Copyright 2021 Vesoft Inc. + +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 config + +import ( + "io/ioutil" + "os" + "os/user" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +const ( + ncConfigRelativePath = "/.kube/nebulacluster.config" +) + +type ( + NebulaClusterConfig struct { + Namespace string `json:"namespace,omitempty"` + ClusterName string `json:"clusterName,omitempty"` + } +) + +func NewNebulaClusterConfig(namespace, clusterName string) *NebulaClusterConfig { + return &NebulaClusterConfig{ + Namespace: namespace, + ClusterName: clusterName, + } +} + +func (c *NebulaClusterConfig) LoadFromFile(filename string) error { + var err error + filename, err = c.parseConfigLocation(filename) + if err != nil { + return err + } + + bs, err := ioutil.ReadFile(filename) //nolint: gosec + if err != nil { + return err + } + + err = yaml.Unmarshal(bs, c) + if err != nil { + return err + } + return nil +} + +func (c *NebulaClusterConfig) SaveToFile(filename string) error { + var err error + filename, err = c.parseConfigLocation(filename) + if err != nil { + return err + } + + dir := filepath.Dir(filename) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0o750); err != nil { + return err + } + } + content, err := yaml.Marshal(c) + if err != nil { + return err + } + return ioutil.WriteFile(filename, content, 0o600) +} + +func (c *NebulaClusterConfig) parseConfigLocation(filename string) (string, error) { + if filename != "" { + return filename, nil + } + return DefaultConfigLocation() +} + +func DefaultConfigLocation() (string, error) { + usr, err := user.Current() + if err != nil { + return "", err + } + return usr.HomeDir + ncConfigRelativePath, nil +} diff --git a/pkg/ngctl/executor/executor.go b/pkg/ngctl/executor/executor.go new file mode 100644 index 00000000..87804572 --- /dev/null +++ b/pkg/ngctl/executor/executor.go @@ -0,0 +1,253 @@ +/* +Copyright 2021 Vesoft Inc. + +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 executor + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + watchtools "k8s.io/client-go/tools/watch" + cmdattach "k8s.io/kubectl/pkg/cmd/attach" + "k8s.io/kubectl/pkg/cmd/logs" + "k8s.io/kubectl/pkg/polymorphichelpers" + "k8s.io/kubectl/pkg/util/interrupt" + + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util/ignore" +) + +type ( + PodExecutor struct { + Pod *corev1.Pod + ContainerName string + KubeCli kubernetes.Interface + RestClientGetter genericclioptions.RESTClientGetter + RestConfig *rest.Config + + genericclioptions.IOStreams + } +) + +func NewPodExecutor( + pod *corev1.Pod, containerName string, + restClientGetter genericclioptions.RESTClientGetter, + ioStreams genericclioptions.IOStreams, +) *PodExecutor { + return &PodExecutor{ + Pod: pod, + ContainerName: containerName, + RestClientGetter: restClientGetter, + IOStreams: ioStreams, + } +} + +func (e *PodExecutor) Execute(ctx context.Context) error { + var err error + e.RestConfig, err = e.RestClientGetter.ToRESTConfig() + if err != nil { + return err + } + e.KubeCli, err = kubernetes.NewForConfig(e.RestConfig) + if err != nil { + return err + } + + pod, err := e.KubeCli.CoreV1().Pods(e.Pod.Namespace).Create(ctx, e.Pod, metav1.CreateOptions{}) + if err != nil { + return err + } + e.Pod = pod + + if e.ContainerName == "" { + container, err := e.getContainer() + if err != nil { + return err + } + e.ContainerName = container.Name + } + + defer func() { + _ = e.destroy(ctx) + }() + return e.attachPod(ctx) +} + +func (e *PodExecutor) destroy(ctx context.Context) error { + return e.removePod(ctx) +} + +func (e *PodExecutor) removePod(ctx context.Context) error { + return e.KubeCli.CoreV1().Pods(e.Pod.Namespace).Delete(ctx, e.Pod.Name, metav1.DeleteOptions{}) +} + +func (e *PodExecutor) attachPod(ctx context.Context) error { + pod, err := e.waitForContainer(ctx) + if err != nil { + return err + } + + e.Pod = pod + attachOpts := cmdattach.NewAttachOptions(e.IOStreams) + + attachOpts.Pod = pod + attachOpts.PodName = pod.Name + attachOpts.Namespace = pod.Namespace + attachOpts.ContainerName = e.ContainerName + attachOpts.Config = e.RestConfig + attachOpts.StreamOptions.Stdin = true + attachOpts.StreamOptions.TTY = true + attachOpts.StreamOptions.Quiet = false + + status := e.getContainerStatus() + if status == nil { + return fmt.Errorf("error getting container status of container name %q: %+v", e.ContainerName, err) + } + if status.State.Terminated != nil { + ignore.Fprintf(e.ErrOut, "Container terminated, falling back to logs") + return e.logBack() + } + + if err := attachOpts.Run(); err != nil { + ignore.Fprintf(e.ErrOut, "Error attaching, falling back to logs: %v\n", err) + return e.logBack() + } + return nil +} + +func (e *PodExecutor) waitForContainer(ctx context.Context) (*corev1.Pod, error) { + ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, 0*time.Second) + defer cancel() + + pod := e.Pod + podClient := e.KubeCli.CoreV1() + + fieldSelector := fields.OneTermEqualSelector("metadata.name", pod.Name).String() + lw := &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + options.FieldSelector = fieldSelector + return podClient.Pods(pod.Namespace).List(ctx, options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + options.FieldSelector = fieldSelector + return podClient.Pods(pod.Namespace).Watch(ctx, options) + }, + } + + intr := interrupt.New(nil, cancel) + var result *corev1.Pod + err := intr.Run(func() error { + ev, err := watchtools.UntilWithSync(ctx, lw, &corev1.Pod{}, nil, func(ev watch.Event) (bool, error) { + if ev.Type == watch.Deleted { + return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") + } + pod, ok := ev.Object.(*corev1.Pod) + if !ok { + return false, fmt.Errorf("watch did not return a pod: %v", ev.Object) + } + e.Pod = pod + + s := e.getContainerStatus() + if s == nil { + return false, nil + } + if s.State.Running != nil || s.State.Terminated != nil { + return true, nil + } + return false, nil + }) + if ev != nil { + result = ev.Object.(*corev1.Pod) + } + return err + }) + + return result, err +} + +func (e *PodExecutor) getContainer() (*corev1.Container, error) { + pod := e.Pod + containerName := e.ContainerName + if containerName != "" { + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == containerName { + return &pod.Spec.Containers[i], nil + } + } + for i := range pod.Spec.InitContainers { + if pod.Spec.InitContainers[i].Name == containerName { + return &pod.Spec.InitContainers[i], nil + } + } + for i := range pod.Spec.EphemeralContainers { + if pod.Spec.EphemeralContainers[i].Name == containerName { + return (*corev1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon), nil + } + } + return nil, fmt.Errorf("container not found (%s)", e.ContainerName) + } + + return &pod.Spec.Containers[0], nil +} + +func (e *PodExecutor) getContainerStatus() *corev1.ContainerStatus { + pod := e.Pod + allContainerStatus := [][]corev1.ContainerStatus{ + pod.Status.InitContainerStatuses, + pod.Status.ContainerStatuses, + pod.Status.EphemeralContainerStatuses, + } + for _, statusSlice := range allContainerStatus { + for i := range statusSlice { + if statusSlice[i].Name == e.ContainerName { + return &statusSlice[i] + } + } + } + return nil +} + +func (e *PodExecutor) logBack() error { + pod := e.Pod + container, err := e.getContainer() + if err != nil { + return err + } + + requests, err := polymorphichelpers.LogsForObjectFn(e.RestClientGetter, pod, + &corev1.PodLogOptions{Container: container.Name}, 5*time.Second, false) + if err != nil { + return err + } + for _, request := range requests { + if err := logs.DefaultConsumeRequest(request, e.Out); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 345c15b0..2bbf6f17 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -22,13 +22,12 @@ import ( "fmt" "regexp" "strings" - - "k8s.io/klog/v2" ) var paramPattern = regexp.MustCompile(`--(\w+)=(.+)`) func AppendCustomConfig(data string, custom map[string]string) string { + log := getLog() if len(custom) == 0 { return data } @@ -48,7 +47,7 @@ func AppendCustomConfig(data string, custom map[string]string) string { if strings.HasPrefix(line, "--") { match := paramPattern.FindStringSubmatch(line) if len(match) != 3 { - klog.Errorf("%s not match param pattern", line) + log.Info("not match param pattern", "line", line) continue } param, value := match[1], match[2] @@ -61,7 +60,7 @@ func AppendCustomConfig(data string, custom map[string]string) string { } } if err := scanner.Err(); err != nil { - klog.Errorf("reading input failed:", err) + log.Error(err, "reading input failed") } if len(custom) > 0 { diff --git a/pkg/util/config/log.go b/pkg/util/config/log.go new file mode 100644 index 00000000..39c472c3 --- /dev/null +++ b/pkg/util/config/log.go @@ -0,0 +1,31 @@ +/* +Copyright 2021 Vesoft Inc. + +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 config + +import ( + "github.com/go-logr/logr" + + "github.com/vesoft-inc/nebula-operator/pkg/logging" +) + +// Please don't use directly, but use getLog. +// Examples: +// log := getLog().WithName("name").WithValues("key", "value") +// log.Info(...) +var _log = logging.Log.WithName("webhook") + +func getLog() logr.Logger { return _log }