diff --git a/cmd/virt-api/virt-api.go b/cmd/virt-api/virt-api.go index 06d95c957922..aceee0a5367b 100644 --- a/cmd/virt-api/virt-api.go +++ b/cmd/virt-api/virt-api.go @@ -7,7 +7,9 @@ import ( "golang.org/x/net/context" "k8s.io/client-go/pkg/runtime/schema" "kubevirt.io/kubevirt/pkg/api/v1" + "kubevirt.io/kubevirt/pkg/kubecli" "kubevirt.io/kubevirt/pkg/logging" + "kubevirt.io/kubevirt/pkg/rest/endpoints" "kubevirt.io/kubevirt/pkg/virt-api/rest" "log" "net/http" @@ -31,6 +33,20 @@ func main() { if err != nil { log.Fatal(err) } + cli, err := kubecli.GetRESTClient() + if err != nil { + log.Fatal(err) + } + coreCli, err := kubecli.Get() + if err != nil { + log.Fatal(err) + } + + spice := endpoints.MakeGoRestfulWrapper(endpoints.NewHandlerBuilder().Get(). + Endpoint(rest.NewSpiceSubResourceEndpoint(cli, coreCli, gvr)).Encoder(endpoints.EncodePlainTextGetResponse).Build(ctx)) + rest.WebService.Route(rest.WebService.GET(rest.SubResourcePath(gvr, "spice")). + To(spice).Produces("text/plain"). + Doc("Returns a remote-viewer configuration file. Run `man 1 remote-viewer` to learn more about the configuration format.")) config := swagger.Config{ WebServices: restful.RegisteredWebServices(), // you control what services are visible diff --git a/images/haproxy/haproxy.cfg b/images/haproxy/haproxy.cfg index 2b8cc5270c86..286d01f07e73 100644 --- a/images/haproxy/haproxy.cfg +++ b/images/haproxy/haproxy.cfg @@ -4,14 +4,17 @@ resolvers kubernetes timeout retry 2s hold valid 30s -frontend http +frontend POST_PUT bind *:8184 mode http - acl is_kubevirt_methods method POST PUT + acl is_post_or_put method POST PUT + acl is_get method GET acl is_kubevirt_path path_beg -i /apis/kubevirt.io/v1alpha1 + acl is_spice path_end -i spice http-request add-header Authorization Bearer\ %[env(TOKEN)] timeout client 1m - use_backend srvs_kubevirt if is_kubevirt_methods is_kubevirt_path + use_backend srvs_kubevirt if is_post_or_put is_kubevirt_path + use_backend srvs_kubevirt if is_get is_spice default_backend srvs_apiserver backend srvs_kubevirt diff --git a/manifests/virt-api.yaml.in b/manifests/virt-api.yaml.in index f1ed32b9442e..3ab37dd64d1c 100644 --- a/manifests/virt-api.yaml.in +++ b/manifests/virt-api.yaml.in @@ -26,6 +26,8 @@ spec: - "/virt-api" - "--port" - "8183" + - "--spice-proxy" + - "{{ master_ip }}:3128" ports: - containerPort: 8183 name: "virt-api" diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 42cd6501647a..6bdd7bd5bedf 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -43,6 +43,7 @@ type ResourceExistsError struct{ appError } type ResourceNotFoundError struct{ appError } // Can be thrown before or by a service call type PreconditionError struct{ appError } // Precondition not met, most likely a bug in a service (service) type InternalServerError struct{ appError } // Unknown internal error, most likely a bug in a service or a library +type BadRequestError struct{ appError } type KubernetesError struct { result rest.Result @@ -127,8 +128,12 @@ func InternalErrorMiddleware(logger log.Logger) endpoint.Middleware { } } -func NewResourceNotFoundError(resource string, name string) *ResourceNotFoundError { - return &ResourceNotFoundError{appError{err: fmt.Errorf("%s with name %s does not exist", resource, name)}} +func NewResourceNotFoundError(msg string) *ResourceNotFoundError { + return &ResourceNotFoundError{appError{err: fmt.Errorf(msg)}} +} + +func NewBadRequestError(msg string) *BadRequestError { + return &BadRequestError{appError{err: fmt.Errorf(msg)}} } func NewResourceExistsError(resource string, name string) *ResourceNotFoundError { diff --git a/pkg/rest/endpoints/encoders.go b/pkg/rest/endpoints/encoders.go index 7a0b42e8130e..b121e4eb5b41 100644 --- a/pkg/rest/endpoints/encoders.go +++ b/pkg/rest/endpoints/encoders.go @@ -20,6 +20,9 @@ func encodeApplicationErrors(_ context.Context, w http.ResponseWriter, response case *middleware.ResourceNotFoundError: w.WriteHeader(http.StatusNotFound) _, err = w.Write([]byte(t.Cause().Error())) + case middleware.BadRequestError: + w.WriteHeader(http.StatusBadRequest) + _, err = w.Write([]byte(t.Cause().Error())) case middleware.ResourceExistsError: w.WriteHeader(http.StatusConflict) _, err = w.Write([]byte(t.Cause().Error())) @@ -49,6 +52,16 @@ func EncodeGetResponse(context context.Context, w http.ResponseWriter, response return encodeJsonResponse(w, response, http.StatusOK) } +func EncodePlainTextGetResponse(context context.Context, w http.ResponseWriter, response interface{}) error { + if _, ok := response.(middleware.AppError); ok != false { + return encodeApplicationErrors(context, w, response) + } + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(response.(string))) + return err +} + func EncodeDeleteResponse(context context.Context, w http.ResponseWriter, response interface{}) error { return EncodeGetResponse(context, w, response) } diff --git a/pkg/virt-api/rest/kubeproxy.go b/pkg/virt-api/rest/kubeproxy.go index 4bb4eff23e41..f764431591d9 100644 --- a/pkg/virt-api/rest/kubeproxy.go +++ b/pkg/virt-api/rest/kubeproxy.go @@ -28,16 +28,16 @@ func AddGenericResourceProxy(ws *restful.WebService, ctx context.Context, gvr sc post := endpoints.NewHandlerBuilder().Post(ptr).Endpoint(NewGenericPostEndpoint(cli, gvr, response)).Build(ctx) get := endpoints.NewHandlerBuilder().Get().Endpoint(NewGenericGetEndpoint(cli, gvr, response)).Build(ctx) - ws.Route(ws.POST(fmt.Sprintf("apis/%s/%s/namespaces/{namespace}/%s", gvr.Group, gvr.Version, gvr.Resource)). + ws.Route(ws.POST(ResourcePathBase(gvr)). To(endpoints.MakeGoRestfulWrapper(post)).Reads(example).Writes(example)) - ws.Route(ws.PUT(fmt.Sprintf("apis/%s/%s/namespaces/{namespace}/%s/{name}", gvr.Group, gvr.Version, gvr.Resource)). + ws.Route(ws.PUT(ResourcePath(gvr)). To(endpoints.MakeGoRestfulWrapper(put)).Reads(example).Writes(example).Doc("test2")) - ws.Route(ws.DELETE(fmt.Sprintf("apis/%s/%s/namespaces/{namespace}/%s/{name}", gvr.Group, gvr.Version, gvr.Resource)). + ws.Route(ws.DELETE(ResourcePath(gvr)). To(endpoints.MakeGoRestfulWrapper(delete)).Writes(metav1.Status{}).Doc("test3")) - ws.Route(ws.GET(fmt.Sprintf("apis/%s/%s/namespaces/{namespace}/%s/{name}", gvr.Group, gvr.Version, gvr.Resource)). + ws.Route(ws.GET(ResourcePath(gvr)). To(endpoints.MakeGoRestfulWrapper(get)).Writes(example).Doc("test4")) return nil } @@ -88,3 +88,15 @@ func NewResponseHandler(gvk schema.GroupVersionKind, ptr runtime.Object) Respons } } + +func ResourcePathBase(gvr schema.GroupVersionResource) string { + return fmt.Sprintf("apis/%s/%s/namespaces/{namespace}/%s", gvr.Group, gvr.Version, gvr.Resource) +} + +func ResourcePath(gvr schema.GroupVersionResource) string { + return ResourcePathBase(gvr) + "/{name}" +} + +func SubResourcePath(gvr schema.GroupVersionResource, subResource string) string { + return ResourcePath(gvr) + "/" + subResource +} diff --git a/pkg/virt-api/rest/rest.go b/pkg/virt-api/rest/rest.go index d9ccab9312c4..d02a14c806e4 100644 --- a/pkg/virt-api/rest/rest.go +++ b/pkg/virt-api/rest/rest.go @@ -1,12 +1,27 @@ package rest import ( + "flag" + "fmt" "github.com/emicklei/go-restful" + "github.com/go-kit/kit/endpoint" + "golang.org/x/net/context" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/pkg/api" + kubev1 "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/pkg/fields" + "k8s.io/client-go/pkg/labels" + "k8s.io/client-go/pkg/runtime/schema" + "k8s.io/client-go/rest" "kubevirt.io/kubevirt/pkg/api/v1" "kubevirt.io/kubevirt/pkg/healthz" + "kubevirt.io/kubevirt/pkg/middleware" + "kubevirt.io/kubevirt/pkg/rest/endpoints" + "strings" ) var WebService *restful.WebService +var spiceProxy string func init() { WebService = new(restful.WebService) @@ -14,4 +29,70 @@ func init() { WebService.ApiVersion(v1.GroupVersion.String()).Doc("help") restful.Add(WebService) WebService.Route(WebService.GET("/apis/" + v1.GroupVersion.String() + "/healthz").To(healthz.KubeConnectionHealthzFunc).Doc("Health endpoint")) + // TODO should be reloadable, use configmaps and update on every access? Watch a config file and reload? + flag.StringVar(&spiceProxy, "spice-proxy", "", "Spice proxy to use when spice access is requested") +} + +func NewSpiceSubResourceEndpoint(cli *rest.RESTClient, coreCli *kubernetes.Clientset, gvr schema.GroupVersionResource) endpoint.Endpoint { + return func(ctx context.Context, payload interface{}) (interface{}, error) { + metadata := payload.(*endpoints.Metadata) + result := cli.Get().Namespace(metadata.Namespace).Resource(gvr.Resource).Name(metadata.Name).Do() + if result.Error() != nil { + return nil, middleware.NewInternalServerError(result.Error()) + } + obj, err := result.Get() + if err != nil { + return nil, middleware.NewInternalServerError(result.Error()) + } + + vm := obj.(*v1.VM) + + if vm.Status.Phase != v1.Running { + return nil, middleware.NewResourceNotFoundError("VM is not running") + } + + // TODO allow specifying the spice device. For now select the first one. + for _, d := range vm.Spec.Domain.Devices.Graphics { + if strings.ToLower(d.Type) == "spice" { + port := d.Port + podList, err := coreCli.Core().Pods(api.NamespaceDefault).List(unfinishedVMPodSelector(vm)) + if err != nil { + return nil, middleware.NewInternalServerError(err) + } + + // The pod could just have failed now + if len(podList.Items) == 0 { + // TODO is that the right return code? + return nil, middleware.NewResourceNotFoundError("VM is not running") + } + + pod := podList.Items[0] + ip := pod.Status.PodIP + + config := "[virt-viewer]\n" + + "type=spice\n" + + fmt.Sprintf("host=%s\n", ip) + + fmt.Sprintf("port=%d\n", port) + + if len(spiceProxy) > 0 { + config = config + fmt.Sprintf("proxy=http://%s\n", spiceProxy) + } + return config, nil + } + } + + return nil, middleware.NewResourceNotFoundError("No spice device attached to the VM found.") + } +} + +// TODO for now just copied from VMService +func unfinishedVMPodSelector(vm *v1.VM) kubev1.ListOptions { + fieldSelector := fields.ParseSelectorOrDie( + "status.phase!=" + string(kubev1.PodFailed) + + ",status.phase!=" + string(kubev1.PodSucceeded)) + labelSelector, err := labels.Parse(fmt.Sprintf(v1.DomainLabel+" in (%s)", vm.GetObjectMeta().GetName())) + if err != nil { + panic(err) + } + return kubev1.ListOptions{FieldSelector: fieldSelector.String(), LabelSelector: labelSelector.String()} }