diff --git a/DevelopersGuide.md b/DevelopersGuide.md index 9e6d632..16d582d 100644 --- a/DevelopersGuide.md +++ b/DevelopersGuide.md @@ -68,6 +68,14 @@ Airship UI utilizes the Octant plugin to drive some of the functionality. Octan var pluginName = "hello-world" + // HelloWorldPlugin is a required struct to be an octant compliant plugin + type HelloWorldPlugin struct{} + + // return a new hello world struct + func newHelloWorldPlugin() *HelloWorldPlugin { + return &HelloWorldPlugin{} + } + // This is a sample plugin showing the features of Octant's plugin API. func main() { // Remove the prefix from the go logger since Octant will print logs with timestamps. @@ -78,9 +86,11 @@ Airship UI utilizes the Octant plugin to drive some of the functionality. Octan IsModule: true, } + hwp := newHelloWorldPlugin() + // Set up what should happen when Octant calls this plugin. options := []service.PluginOption{ - service.WithNavigation(handleNavigation, initRoutes), + service.WithNavigation(hwp.handleNavigation, hwp.initRoutes), } // Use the plugin service helper to register this plugin. @@ -94,8 +104,8 @@ Airship UI utilizes the Octant plugin to drive some of the functionality. Octan p.Serve() } - // handleNavigation controls what is displayed on the navigation pane - func handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) { + // handles the navigation pane interation + func (hwp *HelloWorldPlugin) handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) { return navigation.Navigation{ Title: "Hello World Plugin", Path: request.GeneratePath(), @@ -105,15 +115,15 @@ Airship UI utilizes the Octant plugin to drive some of the functionality. Octan // initRoutes routes for this plugin. In this example, there is a global catch all route // that will return the content for every single path. - func initRoutes(router *service.Router) { - router.HandleFunc("*", func(request *service.Request) (component.ContentResponse, error) { - contentResponse := component.NewContentResponse(component.TitleFromString("Hello World Title")) - helloWorld := component.NewText("Hello World just some text on the page") - contentResponse.Add(helloWorld) - // NOTE: this will print out messages only if the -v is added to the start of octent or airship - log.Printf("Sample debugging message: %v\n", helloWorld) - return *contentResponse, nil - }) + func (hwp *HelloWorldPlugin) initRoutes(router *service.Router) { + router.HandleFunc("*", hwp.routeHandler) + } + + func (hwp *HelloWorldPlugin) routeHandler(request service.Request) (component.ContentResponse, error) { + contentResponse := component.NewContentResponse(component.TitleFromString("Hello World Title")) + helloWorld := component.NewText("Hello World just some text on the page") + contentResponse.Add(helloWorld) + return *contentResponse, nil } ``` 5. Build the hello-world plugin diff --git a/README.md b/README.md index a675075..40d4a84 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ airshipui is an executable that wraps Octant. When it is launched, it processes necessary custom startup tasks such as reading the airshipctl config file, then normally calls the function to instantiate Octant. This repository also contains airship plugins that will be generated as standard octant plugins, which are separate binaries. +## Plugins + +[OpenStack Plugin documentation](./cmd/openstack/README.md) + ## Developer's Guide Step by step sample installation and more details can be found in the [Developer's Guide](DevelopersGuide.md). \ No newline at end of file diff --git a/cmd/argoui/main.go b/cmd/argoui/main.go index 50cad46..3693cd9 100644 --- a/cmd/argoui/main.go +++ b/cmd/argoui/main.go @@ -14,14 +14,11 @@ func main() { // Remove the prefix from the go logger since Octant will print logs with timestamps. log.SetPrefix("") - description := fmt.Sprintf("Argo UI version %s", environment.Version()) + description := fmt.Sprintf("%s UI version %s", pluginName, environment.Version()) + // Use the plugin service helper to register this plugin. - p, err := plugin.Register(pluginName, description) - if err != nil { - log.Fatal(err) - } + plugin.Register(pluginName, description) // The plugin can log and the log messages will show up in Octant. - log.Printf("%s is starting", pluginName) - p.Serve() + log.Printf("%s plugin is starting", pluginName) } diff --git a/cmd/openstack/README.md b/cmd/openstack/README.md new file mode 100755 index 0000000..6f1e4aa --- /dev/null +++ b/cmd/openstack/README.md @@ -0,0 +1,53 @@ +# AirshipUI OpenStack Plugin Documentation + +## OpenStack Plugin Requirements +In order to make a connection to the OpenStack APIs the system needs enough information to attach to it. There are 2 ways to accomplish this. + +### File Based OpenStack Connection Details + +Create an openstack.json file in the correct location for your system. + +Windows: +``` +C:\Users\\AppData\Local\octant\etc\openstack.json +``` + +Linux: +``` +~/.config/octant/etc/openstack.json +``` + +The file contents should look like this: +``` +{ + "identityEndpoint": "http:///identity/v3", + "username": "", + "password": "", + "tenantName": "", + "domainID": "" +} +``` + +### Environment Based OpenStack Connection Details + +The following environment variables are required to be set in the same shell that is executing the Octant or AirshipUI binary: +``` + OS_USER_DOMAIN_ID + OS_AUTH_URL + OS_PROJECT_DOMAIN_ID + OS_REGION_NAME + OS_PROJECT_NAME + OS_IDENTITY_API_VERSION + OS_TENANT_NAME + OS_TENANT_ID + OS_AUTH_TYPE + OS_PASSWORD + OS_USERNAME + OS_VOLUME_API_VERSION + OS_TOKEN + OS_USERID +``` + +This plugin should now have enough information to start and display data from your OpenStack instance + +The next time you run Airship UI the OpenStack plugin will be available at http://127.0.0.1:7777/#/openstack \ No newline at end of file diff --git a/cmd/openstack/main.go b/cmd/openstack/main.go new file mode 100755 index 0000000..592c4da --- /dev/null +++ b/cmd/openstack/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "log" + + "opendev.org/airship/airshipui/internal/environment" + plugin "opendev.org/airship/airshipui/internal/plugin/openstack" +) + +var pluginName = "openstack" + +func main() { + // Remove the prefix from the go logger since Octant will print logs with timestamps. + log.SetPrefix("") + + description := fmt.Sprintf("%s version %s", pluginName, environment.Version()) + + // Use the plugin service helper to register this plugin. + plugin.Register(pluginName, description) + + // The plugin can log and the log messages will show up in Octant. + log.Printf("%s is starting", pluginName) +} diff --git a/go.mod b/go.mod index 8e5c7e8..2169a6f 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.12 require ( github.com/go-cmd/cmd v1.2.0 + github.com/golang/mock v1.4.3 + github.com/gophercloud/gophercloud v0.9.0 github.com/spf13/cobra v0.0.6 github.com/spf13/pflag v1.0.5 - github.com/vmware-tanzu/octant v0.10.2-0.20200320182255-15a53b6af867 + github.com/stretchr/testify v1.5.1 + github.com/vmware-tanzu/octant v0.11.0 k8s.io/api v0.17.3 k8s.io/apimachinery v0.17.3 - opendev.org/airship/airshipctl v0.0.0-20200319213630-b2a602fa07e0 + opendev.org/airship/airshipctl v0.0.0-20200326153008-ffacc190e95c ) replace k8s.io/client-go => k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 diff --git a/go.sum b/go.sum index 34d3629..f8f69ac 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -94,6 +95,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20190703090003-6125c262ffb0 h1:ZMEV8o5EYDSweKafp0aPe65/raLEZ7CF9ab9UDMaIMk= github.com/elazarl/goproxy v0.0.0-20190703090003-6125c262ffb0/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= @@ -152,6 +154,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -162,6 +166,8 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -181,6 +187,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/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/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= @@ -188,12 +196,15 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0= github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gophercloud/gophercloud v0.9.0 h1:eJHQQFguQRv2FatH2d2VXH2ueTe2XzjgjwFjFS7SGcs= +github.com/gophercloud/gophercloud v0.9.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= @@ -306,6 +317,7 @@ github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= @@ -388,6 +400,7 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -395,13 +408,24 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vmware-tanzu/octant v0.10.2-0.20200302175445-a52392c6f48e h1:nayXBqq/FKj9MF3+QbKwfvuInEhuGS3XDUJMRPBgBXo= +github.com/vmware-tanzu/octant v0.10.2-0.20200302175445-a52392c6f48e/go.mod h1:ree4Qu6ULumg1MlGrAG7avEQx3+NjWPc0Zs4nQWOmQE= github.com/vmware-tanzu/octant v0.10.2-0.20200320182255-15a53b6af867 h1:KdLbukBYm0E0yuo6TQ2m5v/zHLlLoxaTF2crGCxqB4U= github.com/vmware-tanzu/octant v0.10.2-0.20200320182255-15a53b6af867/go.mod h1:ree4Qu6ULumg1MlGrAG7avEQx3+NjWPc0Zs4nQWOmQE= +github.com/vmware-tanzu/octant v0.10.2 h1:sqXei1wyxhajUt90HGHPEyRTv12T8Mwv6vyKKdLUYg4= +github.com/vmware-tanzu/octant v0.10.2/go.mod h1:ree4Qu6ULumg1MlGrAG7avEQx3+NjWPc0Zs4nQWOmQE= +github.com/vmware-tanzu/octant v0.11.0 h1:1ganDAIiswCL2QDu/C0pczWoraSMIWMtq79GOCqnrr8= +github.com/vmware-tanzu/octant v0.11.0/go.mod h1:ree4Qu6ULumg1MlGrAG7avEQx3+NjWPc0Zs4nQWOmQE= +github.com/vmware/govmomi v0.20.1/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -455,6 +479,7 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a h1:+HHJiFUXVOIS9mr1ThqkQD1N8vpFCfCShqADBM12KTc= golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -468,6 +493,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -490,9 +517,12 @@ golang.org/x/sys v0.0.0-20190621203818-d432491b9138 h1:t8BZD9RDjkm9/h7yYN6kE8oae golang.org/x/sys v0.0.0-20190621203818-d432491b9138/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e h1:9vRrk9YW2BTzLP0VCB9ZDjU4cPqkg+IDWL7XgxA1yxQ= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -516,6 +546,9 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= @@ -544,6 +577,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -565,6 +599,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= @@ -579,6 +614,8 @@ k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQ k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU= k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= +k8s.io/api v0.18.0 h1:lwYk8Vt7rsVTwjRU6pzEsa9YNhThbmbocQlKvNBB4EQ= +k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= k8s.io/apiextensions-apiserver v0.0.0-20181213153335-0fe22c71c476 h1:Ws9zfxsgV19Durts9ftyTG7TO0A/QLhmu98VqNWLiH8= k8s.io/apiextensions-apiserver v0.0.0-20181213153335-0fe22c71c476/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE= k8s.io/apimachinery v0.0.0-20190313205120-d7deff9243b1/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= @@ -588,6 +625,8 @@ k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWc k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ= k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/apimachinery v0.18.0 h1:fuPfYpk3cs1Okp/515pAf0dNhL66+8zk8RLbSX+EgAE= +k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/cli-runtime v0.0.0-20191114110141-0a35778df828 h1:g5LpwfPTlzVksjrqp3EuSFg2ihl9LZVaRhnMas5sB/s= k8s.io/cli-runtime v0.0.0-20191114110141-0a35778df828/go.mod h1:r9ARs2FUnSgInbeN4+mo9nFzf7oqUtRZ3tcuEcoelR4= k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ= @@ -608,6 +647,8 @@ k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJD k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kubectl v0.0.0-20191114113550-6123e1c827f7 h1:J4a49dHJowmwpCnPsIlbUlh1XE+Elq4P++/uSbKPtR0= k8s.io/kubectl v0.0.0-20191114113550-6123e1c827f7/go.mod h1:MYrMrU6JgEeGVz2sFggJizAfyoRjwsP4iTQcP8iQS00= k8s.io/kubernetes v1.14.0 h1:6T2iAEoOYQnzQb3WvPlUkcczEEXZ7+YPlAO8olwujRw= @@ -625,14 +666,30 @@ modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= opendev.org/airship/airshipctl v0.0.0-20200319213630-b2a602fa07e0 h1:PPW+Zyjw96vwdnrKUnlpOjtajihO/1unrOe8C5bH1ok= opendev.org/airship/airshipctl v0.0.0-20200319213630-b2a602fa07e0/go.mod h1:2Ox64Dscbq8HGJjP9s3Mvpfgcn+lRxBKP7n1vD/RnwE= +opendev.org/airship/airshipctl v0.0.0-20200326153008-ffacc190e95c h1:mZYOp/L+7WWlxr6Epq1v7Hi5t6RKm/gt15pEj2R2bPA= +opendev.org/airship/airshipctl v0.0.0-20200326153008-ffacc190e95c/go.mod h1:ILbbxTS02MO4yh7C5iMtz0KzxYFYwSRgiyPhYQ4tWsY= opendev.org/airship/go-redfish v0.0.0-20200110185254-3ab47e28bae8 h1:wIvgHcZH/l7H/eTIjZXGfqurtarlaAv2CtnO/8xYCis= opendev.org/airship/go-redfish v0.0.0-20200110185254-3ab47e28bae8/go.mod h1:FEjYcb3bYBWGpQIqtvVM0NrT5eyjlCOCj5JNf4lI+6s= +opendev.org/airship/go-redfish v0.0.0-20200318103738-db034d1d753a h1:4ggAMTwpfu/w3ZXOIJ9tfYF37JIYn+eNCA4O10NduZ0= +opendev.org/airship/go-redfish v0.0.0-20200318103738-db034d1d753a/go.mod h1:FEjYcb3bYBWGpQIqtvVM0NrT5eyjlCOCj5JNf4lI+6s= opendev.org/airship/go-redfish/client v0.0.0-20200110185254-3ab47e28bae8 h1:ZDtcVfkn6EzA6Kr5I7C3EuN8qsuByeMPII9TnTx1zXQ= opendev.org/airship/go-redfish/client v0.0.0-20200110185254-3ab47e28bae8/go.mod h1:LPq6GmtxEl3Pwf0Ora40EOm46Hz425T9kjsOfG5HKmQ= +opendev.org/airship/go-redfish/client v0.0.0-20200318103738-db034d1d753a h1:S1dmsP5Cc6OQjAd6OgIKMcNPBiGjh5TDbijVjNE/VGU= +opendev.org/airship/go-redfish/client v0.0.0-20200318103738-db034d1d753a/go.mod h1:s0hwuUpBsRXOrhN0NR+fNVivXGyWgHKpqtyq7qYjpew= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 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/kustomize/v3 v3.2.0 h1:EKcEubO29vCbigcMoNynfyZH+ANWkML2UHWibt1Do7o= sigs.k8s.io/kustomize/v3 v3.2.0/go.mod h1:ztX4zYc/QIww3gSripwF7TBOarBTm5BvyAMem0kCzOE= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 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= diff --git a/internal/plugin/argoui/register.go b/internal/plugin/argoui/register.go index 7992b5a..4e258d9 100644 --- a/internal/plugin/argoui/register.go +++ b/internal/plugin/argoui/register.go @@ -19,15 +19,25 @@ import ( v1 "k8s.io/api/core/v1" ) +// ArgoPlugin is an example of a plugin that registers navigation and action handlers +type ArgoPlugin struct{} + +// NewArgoPlugin returns the basic argo plugin struct as required by octant +func NewArgoPlugin() *ArgoPlugin { + return &ArgoPlugin{} +} + // Register registers the plugin with Octant func Register(name string, description string) (*service.Plugin, error) { + a := NewArgoPlugin() + capabilities := &plugin.Capabilities{ IsModule: true, } // Set up what should happen when Octant calls this plugin. options := []service.PluginOption{ - service.WithNavigation(handleNavigation, initRoutes), + service.WithNavigation(a.handleNavigation, a.initRoutes), } // Use the plugin service helper to register this plugin. @@ -38,7 +48,7 @@ func Register(name string, description string) (*service.Plugin, error) { // be called frequently from Octant. Navigation is a tree of `Navigation` structs. // The plugin can use whatever paths it likes since these paths can be namespaced to the // the plugin. -func handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) { +func (a *ArgoPlugin) handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) { return navigation.Navigation{ Title: "Argo UI", Path: request.GeneratePath(), @@ -48,7 +58,7 @@ func handleNavigation(request *service.NavigationRequest) (navigation.Navigation // initRoutes routes for this plugin. In this example, there is a global catch all route // that will return the content for every single path. -func initRoutes(router *service.Router) { +func (a *ArgoPlugin) initRoutes(router *service.Router) { router.HandleFunc("", func(request service.Request) (component.ContentResponse, error) { response := component.NewContentResponse(component.TitleFromString("Argo UI")) diff --git a/internal/plugin/argoui/register_test.go b/internal/plugin/argoui/register_test.go index 3bca85f..0f969bd 100644 --- a/internal/plugin/argoui/register_test.go +++ b/internal/plugin/argoui/register_test.go @@ -1,19 +1,39 @@ package plugin import ( + "context" "fmt" "testing" + "github.com/golang/mock/gomock" "github.com/vmware-tanzu/octant/pkg/plugin/service" + "github.com/vmware-tanzu/octant/pkg/plugin/service/fake" ) +type fakeRequest struct { + ctrl *gomock.Controller + path, title string +} + +func (f *fakeRequest) DashboardClient() service.Dashboard { + return fake.NewMockDashboard(f.ctrl) +} + +func (f *fakeRequest) Path() string { + return "" +} + +func (f *fakeRequest) Context() context.Context { + return context.TODO() +} + func TestRegister(t *testing.T) { - p, err := Register("argo-ui", "Argo UI test version") + plugin, err := Register("argo-ui", "Argo UI test version") if err != nil { t.Fatalf("Registering the plugin returned an error") } - err = p.Validate() + err = plugin.Validate() if err != nil { t.Fatalf("Validating the plugin returned an error") } @@ -21,8 +41,7 @@ func TestRegister(t *testing.T) { func TestRoutes(t *testing.T) { router := service.NewRouter() - - initRoutes(router) + NewArgoPlugin().initRoutes(router) tests := []struct { path string diff --git a/internal/plugin/openstack/compute.go b/internal/plugin/openstack/compute.go new file mode 100755 index 0000000..af0fbce --- /dev/null +++ b/internal/plugin/openstack/compute.go @@ -0,0 +1,164 @@ +/* +Copyright (c) 2020 AT&T. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package plugin + +import ( + "log" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" + "github.com/vmware-tanzu/octant/pkg/view/component" +) + +// gets OpenStack flavors that are available for the tenant +// https://docs.openstack.org/nova/latest/user/flavors.html +func getFlavors(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + flavors.ListDetail(computeClientHelper(osp), flavors.ListOpts{AccessType: flavors.AllAccess}).EachPage( + func(page pagination.Page) (bool, error) { + flavorList, err := flavors.ExtractFlavors(page) + + if err != nil { + log.Fatalf("compute flavor Error: %s\n", err) + } + + for _, flavor := range flavorList { + name := flavor.Name + flavorCache[flavor.ID] = name + + rows = append(rows, component.TableRow{ + "Name": component.NewText(name), + "VCPUs": component.NewText(strconv.Itoa(flavor.VCPUs)), + "RAM": component.NewText(strconv.Itoa(flavor.RAM)), + "Root Disk": component.NewText(strconv.Itoa(flavor.Disk)), + "Ephemeral Disk": component.NewText(strconv.Itoa(flavor.Ephemeral)), + "Swap Disk": component.NewText(strconv.Itoa(flavor.Swap)), + "RX/TX factor": component.NewText(strconv.FormatFloat(flavor.RxTxFactor, 'f', 1, 64)), + "Public": component.NewText(strconv.FormatBool(flavor.IsPublic)), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Flavors", + "No flavors found", + component.NewTableCols("Name", "VCPUs", "RAM", "Root Disk", "Ephemeral Disk", + "Swap Disk", "RX/TX factor", "Public"), + rows) +} + +// gets OpenStack images that are available for the tenant +// https://docs.openstack.org/image-guide/ +func getImages(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + images.ListDetail(computeClientHelper(osp), images.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + list, err := images.ExtractImages(page) + + if err != nil { + log.Fatalf("Image list error: %s\n", err) + return false, err + } + + for _, image := range list { + name := image.Name + imageCache[image.ID] = name + + rows = append(rows, component.TableRow{ + "Name": component.NewText(name), + "Type": component.NewText(strconv.Itoa(image.Progress)), + "Status": component.NewText(image.Status), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Images", + "No images found", + component.NewTableCols("Name", "Type", "Status"), rows) +} + +// gets OpenStack vms that are configured for the tenant +// https://docs.openstack.org/python-openstackclient/latest/cli/command-objects/server.html +func getVMs(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + servers.List(computeClientHelper(osp), servers.ListOpts{AllTenants: true}).EachPage( + func(page pagination.Page) (bool, error) { + list, err := servers.ExtractServers(page) + + if err != nil { + log.Fatalf("Server list error: %s\n", err) + return false, err + } + + for _, server := range list { + name := server.Name + vmCache[server.ID] = name + + // pull together the addresses from the structure + var addresses string + var tmp []string + for subnetName := range server.Addresses { + for _, v := range server.Addresses[subnetName].([]interface{}) { + tmp = append(tmp, v.(map[string]interface{})["addr"].(string)) + } + } + + if len(tmp) > 1 { + addresses = strings.Join(tmp, ", ") + } else { + addresses = tmp[0] + } + + rows = append(rows, component.TableRow{ + "Instance Name": component.NewText(name), + "Image Name": component.NewText(imageCache[server.Image["id"].(string)]), + "IP Address": component.NewText(addresses), + "Flavor": component.NewText(flavorCache[server.Flavor["id"].(string)]), + "Key Pair": component.NewText(server.KeyName), + "Status": component.NewText(server.Status), + "Created": component.NewText(server.Created.Local().String()), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Servers", + "No servers found", + component.NewTableCols("Instance Name", "Image Name", "IP Address", "Flavor", "Key Pair", "Status", "Created"), rows) +} + +// helper function to create a compute specific gophercloud client +func computeClientHelper(osp *OpenstackPlugin) *gophercloud.ServiceClient { + client, err := openstack.NewComputeV2(osp.provider, gophercloud.EndpointOpts{ + Type: "compute", + }) + + if err != nil { + log.Fatalf("Compute Client Error: %s\n", err) + return nil + } + + return client +} diff --git a/internal/plugin/openstack/identity.go b/internal/plugin/openstack/identity.go new file mode 100755 index 0000000..ce41cd1 --- /dev/null +++ b/internal/plugin/openstack/identity.go @@ -0,0 +1,138 @@ +/* +Copyright (c) 2020 AT&T. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package plugin + +import ( + "log" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/identity/v3/domains" + "github.com/gophercloud/gophercloud/openstack/identity/v3/projects" + "github.com/gophercloud/gophercloud/openstack/identity/v3/users" + "github.com/gophercloud/gophercloud/pagination" + "github.com/vmware-tanzu/octant/pkg/view/component" +) + +// gets OpenStack domains that are viewable by the tenant +// https://docs.openstack.org/security-guide/identity/domains.html +func getDomains(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + domains.List(identityClientHelper(osp), domains.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + domainList, err := domains.ExtractDomains(page) + + if err != nil { + log.Fatalf("Broken at user list %v\n", err) + return false, err + } + + for _, domain := range domainList { + name := domain.Name + domainCache[domain.ID] = name + rows = append(rows, component.TableRow{ + "Name": component.NewText(name), + "Description": component.NewText(domain.Description), + "Enabled": component.NewText(strconv.FormatBool(domain.Enabled)), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Domains", + "No domains found", + component.NewTableCols("Name", "Description", "Enabled"), rows) +} + +// gets OpenStack projects that are viewable by the tenant +// https://docs.openstack.org/keystone/latest/admin/cli-manage-projects-users-and-roles.html +func getProjects(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + projects.List(identityClientHelper(osp), projects.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + projectList, err := projects.ExtractProjects(page) + + if err != nil { + log.Fatalf("Broken at project list %v\n", err) + return false, err + } + + for _, project := range projectList { + name := project.Name + projectCache[project.ID] = name + + rows = append(rows, component.TableRow{ + "Enabled": component.NewText(strconv.FormatBool(project.Enabled)), + "Description": component.NewText(project.Description), + "Name": component.NewText(name), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Projects", + "No projects found", + component.NewTableCols("Name", "Enabled", "Description"), rows) +} + +// gets OpenStack uesrs that are viewable by the tenant +// https://docs.openstack.org/keystone/latest/user/index.html +func getUsers(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + users.List(identityClientHelper(osp), users.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + networkList, err := users.ExtractUsers(page) + + if err != nil { + log.Fatalf("Broken at user list %v\n", err) + return false, err + } + + for _, user := range networkList { + email := "" + if user.Extra["email"] != nil { + email = user.Extra["email"].(string) + } + + rows = append(rows, component.TableRow{ + "Name": component.NewText(user.Name), + "Email": component.NewText(email), + "Description": component.NewText(user.Description), + "Enabled": component.NewText(strconv.FormatBool(user.Enabled)), + "Domain Name": component.NewText(domainCache[user.DomainID]), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Users", + "No users found", + component.NewTableCols("Name", "Email", "Description", "Enabled", "Domain Name"), rows) +} + +// helper function to create an identity specific gophercloud client +func identityClientHelper(osp *OpenstackPlugin) *gophercloud.ServiceClient { + client, err := openstack.NewIdentityV3(osp.provider, gophercloud.EndpointOpts{}) + + if err != nil { + log.Fatalf("Identity Client Error: %s\n", err) + return nil + } + + return client +} diff --git a/internal/plugin/openstack/network.go b/internal/plugin/openstack/network.go new file mode 100755 index 0000000..b642e69 --- /dev/null +++ b/internal/plugin/openstack/network.go @@ -0,0 +1,124 @@ +/* +Copyright (c) 2020 AT&T. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package plugin + +import ( + "log" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" + "github.com/gophercloud/gophercloud/pagination" + "github.com/vmware-tanzu/octant/pkg/view/component" +) + +// gets OpenStack networks available for the tenant +// https://docs.openstack.org/api-ref/network/v2/#networks +func getNetworks(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + networks.List(networkClientHelper(osp), networks.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + networkList, err := networks.ExtractNetworks(page) + + if err != nil { + log.Fatalf("Network retrival error: %s\n", err) + return false, err + } + + for _, n := range networkList { + // pull together the subnet names from the cache + var subnetString string + if len(n.Subnets) > 1 { + var tmp []string + for subnetID := range n.Subnets { + name := subnetCache[n.Subnets[subnetID]] + if len(name) > 0 { + tmp = append(tmp, name) + } + } + subnetString = strings.Join(tmp, ", ") + } else { + subnetString = subnetCache[n.Subnets[0]] + } + + rows = append(rows, component.TableRow{ + "Name": component.NewText(n.Name), + "Project": component.NewText(projectCache[n.ProjectID]), + "Subnets": component.NewText(subnetString), + "Shared": component.NewText(strconv.FormatBool(n.Shared)), + "Status": component.NewText(n.Status), + "Admin State": component.NewText(strconv.FormatBool(n.AdminStateUp)), + "Availability Zones": component.NewText(strings.Join(n.AvailabilityZoneHints, ", ")), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Networks", + "No networks found", + component.NewTableCols("Name", "Project", "Subnets", "Shared", "Status", + "Admin State", "Availability Zones"), + rows) +} + +// gets OpenStack subnets available for the tenant +// https://docs.openstack.org/api-ref/network/v2/#subnets +func getSubnets(osp *OpenstackPlugin) component.Component { + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + subnets.List(networkClientHelper(osp), subnets.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + networkList, err := subnets.ExtractSubnets(page) + + if err != nil { + log.Fatalf("Subnet list error: %s\n", err) + return false, err + } + + for _, subnet := range networkList { + cidr := subnet.CIDR + name := subnet.Name + subnetCache[subnet.ID] = name + ": " + cidr + + rows = append(rows, component.TableRow{ + "Name": component.NewText(name), + "Network Address": component.NewText(cidr), + "IP Version": component.NewText("IPv" + strconv.Itoa(subnet.IPVersion)), + "Gateway IP": component.NewText(subnet.GatewayIP), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Subnets", + "No subnets found", + component.NewTableCols("Name", "Network Address", "IP Version", "Gateway IP"), rows) +} + +// helper function to create a network specific gophercloud client +func networkClientHelper(osp *OpenstackPlugin) *gophercloud.ServiceClient { + client, err := openstack.NewNetworkV2(osp.provider, gophercloud.EndpointOpts{ + Name: "neutron", + Region: "RegionOne", + }) + + if err != nil { + log.Fatalf("Network Client Error: %s\n", err) + return nil + } + + return client +} diff --git a/internal/plugin/openstack/register.go b/internal/plugin/openstack/register.go new file mode 100755 index 0000000..4476e29 --- /dev/null +++ b/internal/plugin/openstack/register.go @@ -0,0 +1,197 @@ +/* +Copyright (c) 2020 AT&T. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package plugin + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "runtime" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/vmware-tanzu/octant/pkg/navigation" + "github.com/vmware-tanzu/octant/pkg/plugin" + "github.com/vmware-tanzu/octant/pkg/plugin/service" + "github.com/vmware-tanzu/octant/pkg/view/component" +) + +// OpenstackPlugin is an example of a plugin that registers navigation and action handlers +// and performs some action against a remote API. +type OpenstackPlugin struct { + provider *gophercloud.ProviderClient +} + +// The gophercloud.AuthOptions struct ignores several key variables when attempting to unmarshal the json +// this is a workaround until a pull request can be created / accepted for gophercloud +// they may have reasons to not expose these variables, but it's not clear as to why this is the case +type optJSON struct { + IdentityEndpoint string `json:"identityEndpoint"` + Username string `json:"username"` + Password string `json:"password"` + DomainID string `json:"domainID"` + TenantName string `json:"tenantName"` +} + +// domainCache keeps a record of the domains for use with other reporting features +// This is updated whenever we go into the getDomains compute function +var domainCache = map[string]string{} + +// projectCache keeps a record of the flavors for use with other reporting features +// This is updated whenever we go into the getFlavors compute function +var flavorCache = map[string]string{} + +// imageCache keeps a record of the images for use with other reporting features +// This is updated whenever we go into the getImages compute function +var imageCache = map[string]string{} + +// projectCache keeps a record of the projects for use with other reporting features +// This is updated whenever we go into the getProjects identity function +var projectCache = map[string]string{} + +// subnetCache keeps a record of the subnets for use with other reporting features +// This is updated whenever we go into the getSubnets network function +var subnetCache = map[string]string{} + +// vmCache keeps a record of the servers for use with other reporting features +// This is updated whenever we go into the getVms compute function +var vmCache = map[string]string{} + +// NewOpenstackPlugin authenticates to Openstack and return a new openstack plugin struct +// this requires an openstack.json in the home dir of the user running the process +func NewOpenstackPlugin() *OpenstackPlugin { + opts, err := getOpenstackOpts() + if err != nil { + log.Printf("Error getting authentication opts %s\n", err) + } + + client, err := openstack.AuthenticatedClient(opts) + if err != nil { + log.Printf("Error establishing client %s\n", err) + } + + return &OpenstackPlugin{client} +} + +// helper function to bring in the connection opts for OpenStack +func getOpenstackOpts() (gophercloud.AuthOptions, error) { + /* The following environment variables are required for this function to work + * - OS_USER_DOMAIN_ID + * - OS_AUTH_URL + * - OS_PROJECT_DOMAIN_ID + * - OS_REGION_NAME + * - OS_PROJECT_NAME + * - OS_IDENTITY_API_VERSION + * - OS_TENANT_NAME + * - OS_TENANT_ID + * - OS_AUTH_TYPE + * - OS_PASSWORD + * - OS_USERNAME + * - OS_VOLUME_API_VERSION + * - OS_TOKEN + * - OS_USERID + */ + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + // fall back on file if environment isn't setup + opts, err = getOptsFromFile() + } + + return opts, err +} + +// fall back function if the environment isn't setup, hopefully file based is +func getOptsFromFile() (gophercloud.AuthOptions, error) { + var fileName string + home, err := os.UserHomeDir() + if err != nil { + log.Println(err) + } + if runtime.GOOS == "windows" { + fileName = home + "\\AppData\\Local\\octant\\etc\\openstack.json" + } else { + fileName = home + "/.config/octant/etc/openstack.json" + } + + jsonFile, err := os.Open(fileName) + defer jsonFile.Close() + + if err != nil { + log.Printf("Error opening file %s\n", err) + } + + byteValue, _ := ioutil.ReadAll(jsonFile) + + var tmp optJSON + json.Unmarshal(byteValue, &tmp) + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: tmp.IdentityEndpoint, + Username: tmp.Username, + Password: tmp.Password, + DomainID: tmp.DomainID, + TenantName: tmp.TenantName, + AllowReauth: true, + } + + return opts, err +} + +// Register the plugin with octant +func Register(name string, description string) (*service.Plugin, error) { + osp := NewOpenstackPlugin() + + // Remove the prefix from the go logger since Octant will print logs with timestamps. + log.SetPrefix("") + + // Tell Octant to call this plugin when printing configuration or tabs for Pods + capabilities := &plugin.Capabilities{ + IsModule: true, + } + + // Set up what should happen when Octant calls this plugin. + options := []service.PluginOption{ + service.WithNavigation(osp.handleNavigation, osp.initRoutes), + } + + // Use the plugin service helper to register this plugin. + return service.Register(name, description, capabilities, options...) +} + +// handlePrint creates a navigation tree for this plugin. Navigation is dynamic and will +// be called frequently from Octant. Navigation is a tree of `Navigation` structs. +// The plugin can use whatever paths it likes since these paths can be namespaced to the +// the plugin. +func (osp *OpenstackPlugin) handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) { + return navigation.Navigation{ + Title: "OpenStack", + Path: request.GeneratePath(), + IconName: "cloud", + }, nil +} + +// initRoutes routes for this plugin. In this example, there is a global catch all route +// that will return the content for every single path. +func (osp *OpenstackPlugin) initRoutes(router *service.Router) { + router.HandleFunc("", osp.routeHandler) +} + +// Adds the OpenStack components to the visualization +func (osp *OpenstackPlugin) routeHandler(request service.Request) (component.ContentResponse, error) { + response := component.NewContentResponse(component.TitleFromString("OpenStack")) + response.Add(getDomains(osp)) + response.Add(getUsers(osp)) + response.Add(getFlavors(osp)) + response.Add(getSubnets(osp)) + response.Add(getNetworks(osp)) + response.Add(getImages(osp)) + response.Add(getProjects(osp)) + response.Add(getVMs(osp)) + response.Add(getVolumes(osp)) + + return *response, nil +} diff --git a/internal/plugin/openstack/register_test.go b/internal/plugin/openstack/register_test.go new file mode 100755 index 0000000..cb05626 --- /dev/null +++ b/internal/plugin/openstack/register_test.go @@ -0,0 +1,78 @@ +/* +Copyright (c) 2020 AT&T. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package plugin + +import ( + "context" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/vmware-tanzu/octant/pkg/plugin/service" + "github.com/vmware-tanzu/octant/pkg/plugin/service/fake" +) + +type fakeRequest struct { + ctrl *gomock.Controller + path, title string +} + +func (f *fakeRequest) DashboardClient() service.Dashboard { + return fake.NewMockDashboard(f.ctrl) +} + +func (f *fakeRequest) Path() string { + return "" +} + +func (f *fakeRequest) Context() context.Context { + return context.TODO() +} + +func TestRegister(t *testing.T) { + plugin, err := Register("openstack", "OpenStack test version") + if err != nil { + t.Fatalf("Registering the plugin returned an error") + } + + err = plugin.Validate() + if err != nil { + t.Fatalf("Validating the plugin returned an error") + } +} + +func TestRoutes(t *testing.T) { + router := service.NewRouter() + NewOpenstackPlugin().initRoutes(router) + + tests := []struct { + path string + exists bool + }{ + { + path: "", + exists: true, + }, + { + path: "/not-real", + exists: false, + }, + } + + for _, test := range tests { + test := test // pin the value so that the following function literal binds to it + t.Run(fmt.Sprintf("Path='%s'", test.path), func(t *testing.T) { + _, found := router.Match(test.path) + + if test.exists != found { + if found { + t.Errorf("Found path '%s' when it should not exist.", test.path) + } else { + t.Errorf("Didn't find path '%s' when it should exist.", test.path) + } + } + }) + } +} diff --git a/internal/plugin/openstack/volume.go b/internal/plugin/openstack/volume.go new file mode 100755 index 0000000..d8c3772 --- /dev/null +++ b/internal/plugin/openstack/volume.go @@ -0,0 +1,69 @@ +/* +Copyright (c) 2020 AT&T. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package plugin + +import ( + "log" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" + "github.com/gophercloud/gophercloud/pagination" + "github.com/vmware-tanzu/octant/pkg/view/component" +) + +// gets OpenStack volumes viewable by the tenant +// https://docs.openstack.org/cinder/latest/ +func getVolumes(osp *OpenstackPlugin) component.Component { + client, err := openstack.NewBlockStorageV2(osp.provider, gophercloud.EndpointOpts{}) + + if err != nil { + log.Fatalf("NewIdentityV3 error: %v", err) + } + + rows := []component.TableRow{} + + // TODO: Determine if the error needs to be handled from this function + volumes.List(client, volumes.ListOpts{}).EachPage( + func(page pagination.Page) (bool, error) { + volumeList, err := volumes.ExtractVolumes(page) + + if err != nil { + log.Fatalf("Broken at volume list %v\n", err) + return false, err + } + + for _, volume := range volumeList { + // extract the potentially multiple devices + var attachedDevices []string + for index := range volume.Attachments { + attachment := volume.Attachments[index] + attachedDevices = append(attachedDevices, vmCache[attachment.ServerID]+" at "+attachment.Device) + } + + rows = append(rows, component.TableRow{ + "Name": component.NewText(volume.Name), + "Description": component.NewText(volume.Description), + "Size": component.NewText(strconv.Itoa(volume.Size) + "GiB"), + "Status": component.NewText(volume.Status), + "Type": component.NewText(volume.VolumeType), + "Attached To": component.NewText(strings.Join(attachedDevices, ", ")), + "Availability Zone": component.NewText(volume.AvailabilityZone), + "Bootable": component.NewText(volume.Bootable), + "Encrypted": component.NewText(strconv.FormatBool(volume.Encrypted)), + }) + } + + return true, nil + }) + + return component.NewTableWithRows( + "Volumes", + "No volumes found", + component.NewTableCols("Name", "Description", "Size", "Status", "Group", "Type", "Attached To", + "Availability Zone", "Bootable", "Encrypted"), rows) +} diff --git a/tools/install_linter b/tools/install_linter index 3f03368..b0fa9d0 100755 --- a/tools/install_linter +++ b/tools/install_linter @@ -3,7 +3,7 @@ set -x tools_bin_dir="${BASH_SOURCE%/*}" download_url=https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh -version=v1.21.0 +version=v1.24.0 if ! curl -sfL "$download_url" | sh -s -- -b "$tools_bin_dir/bin" "$version"; then printf "Something went wrong while installing golangci-lint\n" 1>&2