diff --git a/.gitignore b/.gitignore index ed2ad5c520a6..d2b9da3fb279 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ cmd/virt-launcher/virt-launcher* cmd/virt-handler/virt-handler* cmd/virt-api/virt-api* cmd/virtctl/virtctl* +tools/openapispec/openapispec manifests/*.yaml **/bin bin/* diff --git a/.travis.yml b/.travis.yml index 70dc9ed8e9f0..b6f6d481721e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,8 @@ script: - make fmt - if git diff --name-only | grep 'generated.*.go'; then echo "Content of generated files changed. Please regenerate and commit them."; false; fi +- if git diff --name-only | grep 'swagger.json' ; then echo "Content of generated + files changed. Please regenerate and commit them." ; false ; fi - if diff <(git grep -c '') <(git grep -cI '') | egrep -v 'docs/.*\.png|swagger-ui' | grep '^<'; then echo "Binary files are present in git repostory."; false; fi - make check @@ -42,9 +44,6 @@ script: - if [[ $TRAVIS_REPO_SLUG == "kubevirt/kubevirt" ]]; then $HOME/gopath/bin/goveralls -service=travis-ci -package=./pkg/... -ignore=$(find -name generated_mock*.go -printf "%P\n" | paste -d, -s) ; else make test; fi -- make generate-openapi-spec -- if git diff --name-only | grep 'swagger.json' ; then echo "The OpenAPI Specification - was changed. Please run `make generate-openapi-spec` and commit it." ; false ; fi cache: directories: diff --git a/Makefile b/Makefile index c8d52146079b..22e2e72f3b3e 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,13 @@ HASH := md5sum all: build manifests -generate: +generate: sync find pkg/ -name "*generated*.go" -exec rm {} -f \; ./hack/build-go.sh generate ${WHAT} goimports -w -local kubevirt.io cmd/ pkg/ tests/ ./hack/bootstrap-ginkgo.sh + (cd tools/openapispec/ && go build) + tools/openapispec/openapispec --dump-api-spec-path api/openapi-spec/swagger.json build: checksync fmt vet compile @@ -27,16 +29,10 @@ test: build functest: ./hack/build-go.sh functest ${WHAT} -generate-openapi-spec: build - echo -e "apiVersion: v1\nclusters:\n- cluster:\n server: https://127.0.0.1:6443\nkind: Config\n" > .test.kubeconfig - ./cmd/virt-api/virt-api \ - --kubeconfig .test.kubeconfig \ - --dump-api-spec --dump-api-spec-path api/openapi-spec/swagger.json - rm -f .test.kubeconfig - clean: ./hack/build-go.sh clean ${WHAT} rm ./bin -rf + rm tools/openapispec/openapispec -rf distclean: clean rm -rf vendor/ diff --git a/cmd/virt-api/virt-api.go b/cmd/virt-api/virt-api.go index 332dc68e7234..e0b077c68707 100644 --- a/cmd/virt-api/virt-api.go +++ b/cmd/virt-api/virt-api.go @@ -20,175 +20,24 @@ package main import ( - "encoding/json" "flag" - "io/ioutil" - "log" - "net/http" - "github.com/emicklei/go-restful" - restfulspec "github.com/emicklei/go-restful-openapi" - kithttp "github.com/go-kit/kit/transport/http" - openapispec "github.com/go-openapi/spec" "github.com/spf13/pflag" - "golang.org/x/net/context" - "k8s.io/apimachinery/pkg/runtime/schema" - "kubevirt.io/kubevirt/pkg/api/v1" - "kubevirt.io/kubevirt/pkg/healthz" - "kubevirt.io/kubevirt/pkg/kubecli" klog "kubevirt.io/kubevirt/pkg/log" - mime "kubevirt.io/kubevirt/pkg/rest" - "kubevirt.io/kubevirt/pkg/rest/endpoints" - "kubevirt.io/kubevirt/pkg/rest/filter" - "kubevirt.io/kubevirt/pkg/service" - "kubevirt.io/kubevirt/pkg/virt-api/rest" + "kubevirt.io/kubevirt/pkg/virt-api" ) -type virtAPIApp struct { - Service *service.Service - SwaggerUI string -} - -func newVirtAPIApp(host *string, port *int, swaggerUI *string) *virtAPIApp { - return &virtAPIApp{ - Service: service.NewService("virt-api", host, port), - SwaggerUI: *swaggerUI, - } -} - -func (app *virtAPIApp) Compose() { - ctx := context.Background() - vmGVR := schema.GroupVersionResource{Group: v1.GroupVersion.Group, Version: v1.GroupVersion.Version, Resource: "virtualmachines"} - migrationGVR := schema.GroupVersionResource{Group: v1.GroupVersion.Group, Version: v1.GroupVersion.Version, Resource: "migrations"} - vmrsGVR := schema.GroupVersionResource{Group: v1.GroupVersion.Group, Version: v1.GroupVersion.Version, Resource: "virtualmachinereplicasets"} - - ws, err := rest.GroupVersionProxyBase(ctx, v1.GroupVersion) - if err != nil { - log.Fatal(err) - } - - ws, err = rest.GenericResourceProxy(ws, ctx, vmGVR, &v1.VirtualMachine{}, v1.VirtualMachineGroupVersionKind.Kind, &v1.VirtualMachineList{}) - if err != nil { - log.Fatal(err) - } - - ws, err = rest.GenericResourceProxy(ws, ctx, migrationGVR, &v1.Migration{}, v1.MigrationGroupVersionKind.Kind, &v1.MigrationList{}) - if err != nil { - log.Fatal(err) - } - - ws, err = rest.GenericResourceProxy(ws, ctx, vmrsGVR, &v1.VirtualMachineReplicaSet{}, v1.VMReplicaSetGroupVersionKind.Kind, &v1.VirtualMachineReplicaSetList{}) - if err != nil { - log.Fatal(err) - } - - virtCli, err := kubecli.GetKubevirtClient() - if err != nil { - log.Fatal(err) - } - - // TODO, allow Encoder and Decoders per type and combine the endpoint logic - spice := endpoints.MakeGoRestfulWrapper(endpoints.NewHandlerBuilder().Get(). - Endpoint(rest.NewSpiceEndpoint(virtCli.RestClient(), vmGVR)).Encoder( - endpoints.NewMimeTypeAwareEncoder(endpoints.NewEncodeINIResponse(http.StatusOK), - map[string]kithttp.EncodeResponseFunc{ - mime.MIME_INI: endpoints.NewEncodeINIResponse(http.StatusOK), - mime.MIME_JSON: endpoints.NewEncodeJsonResponse(http.StatusOK), - mime.MIME_YAML: endpoints.NewEncodeYamlResponse(http.StatusOK), - })).Build(ctx)) - - ws.Route(ws.GET(rest.ResourcePath(vmGVR)+rest.SubResourcePath("spice")). - To(spice).Produces(mime.MIME_INI, mime.MIME_JSON, mime.MIME_YAML). - Param(rest.NamespaceParam(ws)).Param(rest.NameParam(ws)). - Operation("spice"). - Doc("Returns a remote-viewer configuration file. Run `man 1 remote-viewer` to learn more about the configuration format.")) - - ws.Route(ws.GET(rest.ResourcePath(vmGVR) + rest.SubResourcePath("console")). - To(rest.NewConsoleResource(virtCli, virtCli.CoreV1()).Console). - Param(restful.QueryParameter("console", "Name of the serial console to connect to")). - Param(rest.NamespaceParam(ws)).Param(rest.NameParam(ws)). - Operation("console"). - Doc("Open a websocket connection to a serial console on the specified VM.")) - - restful.Add(ws) - - ws.Route(ws.GET("/healthz").To(healthz.KubeConnectionHealthzFunc).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).Doc("Health endpoint")) - ws, err = rest.ResourceProxyAutodiscovery(ctx, vmGVR) - if err != nil { - log.Fatal(err) - } - - restful.Add(ws) - - restful.Filter(filter.RequestLoggingFilter()) - restful.Filter(restful.OPTIONSFilter()) -} - -func (app *virtAPIApp) ConfigureOpenAPIService() { - restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(createOpenAPIConfig())) - http.Handle("/swagger-ui/", http.StripPrefix("/swagger-ui/", http.FileServer(http.Dir(app.SwaggerUI)))) -} - -func createOpenAPIConfig() restfulspec.Config { - return restfulspec.Config{ - WebServices: restful.RegisteredWebServices(), - WebServicesURL: "http://localhost:8183", - APIPath: "/swaggerapi", - PostBuildSwaggerObjectHandler: addInfoToSwaggerObject, - } -} - -func addInfoToSwaggerObject(swo *openapispec.Swagger) { - swo.Info = &openapispec.Info{ - InfoProps: openapispec.InfoProps{ - Title: "KubeVirt API, ", - Description: "This is KubeVirt API an add-on for Kubernetes.", - Contact: &openapispec.ContactInfo{ - Name: "kubevirt-dev", - Email: "kubevirt-dev@googlegroups.com", - URL: "https://github.com/kubevirt/kubevirt", - }, - License: &openapispec.License{ - Name: "Apache 2.0", - URL: "https://www.apache.org/licenses/LICENSE-2.0", - }, - }, - } -} - -func (app *virtAPIApp) Run() { - log.Fatal(http.ListenAndServe(app.Service.Address(), nil)) -} - -func dumpOpenApiSpec(dumppath *string) { - openapispec := restfulspec.BuildSwagger(createOpenAPIConfig()) - data, err := json.MarshalIndent(openapispec, " ", " ") - if err != nil { - log.Fatal(err) - } - err = ioutil.WriteFile(*dumppath, data, 0644) - if err != nil { - log.Fatal(err) - } -} - func main() { klog.InitializeLogging("virt-api") swaggerui := flag.String("swagger-ui", "third_party/swagger-ui", "swagger-ui location") host := flag.String("listen", "0.0.0.0", "Address and port where to listen on") port := flag.Int("port", 8183, "Port to listen on") - dumpapispec := flag.Bool("dump-api-spec", false, "Dump OpenApi spec, and exit immediately.") - dumpapispecpath := flag.String("dump-api-spec-path", "openapi.json", "Path to OpenApi dump.") pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() - app := newVirtAPIApp(host, port, swaggerui) + app := virt_api.NewVirtAPIApp(*host, *port, *swaggerui) app.Compose() - if *dumpapispec == true { - dumpOpenApiSpec(dumpapispecpath) - } else { - app.ConfigureOpenAPIService() - app.Run() - } + app.ConfigureOpenAPIService() + app.Run() } diff --git a/cmd/virt-handler/virt-handler.go b/cmd/virt-handler/virt-handler.go index ec8c9ae99a67..0f47d5177a54 100644 --- a/cmd/virt-handler/virt-handler.go +++ b/cmd/virt-handler/virt-handler.go @@ -77,7 +77,7 @@ func newVirtHandlerApp(host *string, port *int, hostOverride *string, libvirtUri } return &virtHandlerApp{ - Service: service.NewService("virt-handler", host, port), + Service: service.NewService("virt-handler", *host, *port), HostOverride: *hostOverride, LibvirtUri: *libvirtUri, VirtShareDir: *virtShareDir, diff --git a/cmd/virt-manifest/virt-manifest.go b/cmd/virt-manifest/virt-manifest.go index 4038f8841261..0f4ce5180793 100644 --- a/cmd/virt-manifest/virt-manifest.go +++ b/cmd/virt-manifest/virt-manifest.go @@ -41,7 +41,7 @@ type virtManifestApp struct { func newVirtManifestApp(host *string, port *int, libvirtUri *string) *virtManifestApp { return &virtManifestApp{ - Service: service.NewService("virt-manifest", host, port), + Service: service.NewService("virt-manifest", *host, *port), LibvirtUri: *libvirtUri, } } diff --git a/pkg/service/service.go b/pkg/service/service.go index f61c4480ff6c..f32bea698b8f 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -30,11 +30,11 @@ type Service struct { Port string } -func NewService(name string, host *string, port *int) *Service { +func NewService(name string, host string, port int) *Service { return &Service{ Name: name, - Host: *host, - Port: strconv.Itoa(*port), + Host: host, + Port: strconv.Itoa(port), } } diff --git a/pkg/virt-api/api.go b/pkg/virt-api/api.go new file mode 100644 index 000000000000..629fdf2d419d --- /dev/null +++ b/pkg/virt-api/api.go @@ -0,0 +1,138 @@ +package virt_api + +import ( + "log" + "net/http" + + "github.com/emicklei/go-restful" + "github.com/emicklei/go-restful-openapi" + kithttp "github.com/go-kit/kit/transport/http" + openapispec "github.com/go-openapi/spec" + "golang.org/x/net/context" + "k8s.io/apimachinery/pkg/runtime/schema" + + "kubevirt.io/kubevirt/pkg/api/v1" + "kubevirt.io/kubevirt/pkg/healthz" + "kubevirt.io/kubevirt/pkg/kubecli" + mime "kubevirt.io/kubevirt/pkg/rest" + "kubevirt.io/kubevirt/pkg/rest/endpoints" + "kubevirt.io/kubevirt/pkg/rest/filter" + "kubevirt.io/kubevirt/pkg/service" + "kubevirt.io/kubevirt/pkg/virt-api/rest" +) + +type virtAPIApp struct { + Service *service.Service + SwaggerUI string +} + +func NewVirtAPIApp(host string, port int, swaggerUI string) *virtAPIApp { + return &virtAPIApp{ + Service: service.NewService("virt-api", host, port), + SwaggerUI: swaggerUI, + } +} + +func (app *virtAPIApp) Compose() { + ctx := context.Background() + vmGVR := schema.GroupVersionResource{Group: v1.GroupVersion.Group, Version: v1.GroupVersion.Version, Resource: "virtualmachines"} + migrationGVR := schema.GroupVersionResource{Group: v1.GroupVersion.Group, Version: v1.GroupVersion.Version, Resource: "migrations"} + vmrsGVR := schema.GroupVersionResource{Group: v1.GroupVersion.Group, Version: v1.GroupVersion.Version, Resource: "virtualmachinereplicasets"} + + ws, err := rest.GroupVersionProxyBase(ctx, v1.GroupVersion) + if err != nil { + log.Fatal(err) + } + + ws, err = rest.GenericResourceProxy(ws, ctx, vmGVR, &v1.VirtualMachine{}, v1.VirtualMachineGroupVersionKind.Kind, &v1.VirtualMachineList{}) + if err != nil { + log.Fatal(err) + } + + ws, err = rest.GenericResourceProxy(ws, ctx, migrationGVR, &v1.Migration{}, v1.MigrationGroupVersionKind.Kind, &v1.MigrationList{}) + if err != nil { + log.Fatal(err) + } + + ws, err = rest.GenericResourceProxy(ws, ctx, vmrsGVR, &v1.VirtualMachineReplicaSet{}, v1.VMReplicaSetGroupVersionKind.Kind, &v1.VirtualMachineReplicaSetList{}) + if err != nil { + log.Fatal(err) + } + + virtCli, err := kubecli.GetKubevirtClient() + if err != nil { + log.Fatal(err) + } + + // TODO, allow Encoder and Decoders per type and combine the endpoint logic + spice := endpoints.MakeGoRestfulWrapper(endpoints.NewHandlerBuilder().Get(). + Endpoint(rest.NewSpiceEndpoint(virtCli.RestClient(), vmGVR)).Encoder( + endpoints.NewMimeTypeAwareEncoder(endpoints.NewEncodeINIResponse(http.StatusOK), + map[string]kithttp.EncodeResponseFunc{ + mime.MIME_INI: endpoints.NewEncodeINIResponse(http.StatusOK), + mime.MIME_JSON: endpoints.NewEncodeJsonResponse(http.StatusOK), + mime.MIME_YAML: endpoints.NewEncodeYamlResponse(http.StatusOK), + })).Build(ctx)) + + ws.Route(ws.GET(rest.ResourcePath(vmGVR)+rest.SubResourcePath("spice")). + To(spice).Produces(mime.MIME_INI, mime.MIME_JSON, mime.MIME_YAML). + Param(rest.NamespaceParam(ws)).Param(rest.NameParam(ws)). + Operation("spice"). + Doc("Returns a remote-viewer configuration file. Run `man 1 remote-viewer` to learn more about the configuration format.")) + + ws.Route(ws.GET(rest.ResourcePath(vmGVR) + rest.SubResourcePath("console")). + To(rest.NewConsoleResource(virtCli, virtCli.CoreV1()).Console). + Param(restful.QueryParameter("console", "Name of the serial console to connect to")). + Param(rest.NamespaceParam(ws)).Param(rest.NameParam(ws)). + Operation("console"). + Doc("Open a websocket connection to a serial console on the specified VM.")) + + restful.Add(ws) + + ws.Route(ws.GET("/healthz").To(healthz.KubeConnectionHealthzFunc).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).Doc("Health endpoint")) + ws, err = rest.ResourceProxyAutodiscovery(ctx, vmGVR) + if err != nil { + log.Fatal(err) + } + + restful.Add(ws) + + restful.Filter(filter.RequestLoggingFilter()) + restful.Filter(restful.OPTIONSFilter()) +} + +func (app *virtAPIApp) ConfigureOpenAPIService() { + restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(CreateOpenAPIConfig())) + http.Handle("/swagger-ui/", http.StripPrefix("/swagger-ui/", http.FileServer(http.Dir(app.SwaggerUI)))) +} + +func CreateOpenAPIConfig() restfulspec.Config { + return restfulspec.Config{ + WebServices: restful.RegisteredWebServices(), + WebServicesURL: "", + APIPath: "/swaggerapi", + PostBuildSwaggerObjectHandler: addInfoToSwaggerObject, + } +} + +func addInfoToSwaggerObject(swo *openapispec.Swagger) { + swo.Info = &openapispec.Info{ + InfoProps: openapispec.InfoProps{ + Title: "KubeVirt API, ", + Description: "This is KubeVirt API an add-on for Kubernetes.", + Contact: &openapispec.ContactInfo{ + Name: "kubevirt-dev", + Email: "kubevirt-dev@googlegroups.com", + URL: "https://github.com/kubevirt/kubevirt", + }, + License: &openapispec.License{ + Name: "Apache 2.0", + URL: "https://www.apache.org/licenses/LICENSE-2.0", + }, + }, + } +} + +func (app *virtAPIApp) Run() { + log.Fatal(http.ListenAndServe(app.Service.Address(), nil)) +} diff --git a/tools/openapispec/openapispec.go b/tools/openapispec/openapispec.go new file mode 100644 index 000000000000..75bace3315c9 --- /dev/null +++ b/tools/openapispec/openapispec.go @@ -0,0 +1,59 @@ +/* + * This file is part of the KubeVirt project + * + * 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. + * + * Copyright 2017 Red Hat, Inc. + * + */ + +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "log" + + "github.com/emicklei/go-restful-openapi" + "github.com/spf13/pflag" + + klog "kubevirt.io/kubevirt/pkg/log" + "kubevirt.io/kubevirt/pkg/virt-api" +) + +func dumpOpenApiSpec(dumppath *string) { + openapispec := restfulspec.BuildSwagger(virt_api.CreateOpenAPIConfig()) + data, err := json.MarshalIndent(openapispec, " ", " ") + if err != nil { + log.Fatal(err) + } + err = ioutil.WriteFile(*dumppath, data, 0644) + if err != nil { + log.Fatal(err) + } +} + +func main() { + klog.InitializeLogging("openapispec") + dumpapispecpath := flag.String("dump-api-spec-path", "openapi.json", "Path to OpenApi dump.") + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + // client-go requires a config or a master to be set in order to configure a client + pflag.Set("master", "http://127.0.0.1:4321") + pflag.Parse() + + // arguments for NewVirtAPIApp have no influence on the generated spec + app := virt_api.NewVirtAPIApp("0.0.0.0", 1234, "/swagger") + app.Compose() + dumpOpenApiSpec(dumpapispecpath) +}