Skip to content

Commit

Permalink
Introduce the /spice subresource to vm resources
Browse files Browse the repository at this point in the history
Collect all necessary connection details from the cluster and return a
remote-viewer configuration file. Details about the configuration can be
found at `man 1 remote-viewer`.
  • Loading branch information
rmohr committed Jan 19, 2017
1 parent d1fd0ff commit a497997
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 9 deletions.
16 changes: 16 additions & 0 deletions cmd/virt-api/virt-api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions images/haproxy/haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions manifests/virt-api.yaml.in
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ spec:
- "/virt-api"
- "--port"
- "8183"
- "--spice-proxy"
- "{{ master_ip }}:3128"
ports:
- containerPort: 8183
name: "virt-api"
Expand Down
9 changes: 7 additions & 2 deletions pkg/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions pkg/rest/endpoints/encoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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)
}
Expand Down
20 changes: 16 additions & 4 deletions pkg/virt-api/rest/kubeproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
81 changes: 81 additions & 0 deletions pkg/virt-api/rest/rest.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,98 @@
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)
WebService.Path("/").Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)
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()}
}

0 comments on commit a497997

Please sign in to comment.