diff --git a/go.mod b/go.mod index bf43bf51..3776711e 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,12 @@ require ( sigs.k8s.io/controller-runtime v0.17.2 ) +require ( + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + k8s.io/cli-runtime v0.29.2 // indirect +) + require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -120,9 +126,10 @@ require ( gotest.tools/v3 v3.5.1 // indirect k8s.io/apiextensions-apiserver v0.29.1 // indirect k8s.io/apiserver v0.29.1 // indirect - k8s.io/component-base v0.29.1 // indirect + k8s.io/component-base v0.29.2 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/kubectl v0.29.2 oras.land/oras-go v1.2.4 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index 14219563..34a45d86 100644 --- a/go.sum +++ b/go.sum @@ -178,6 +178,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -185,6 +187,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= @@ -419,14 +423,18 @@ k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= k8s.io/apiserver v0.29.1 h1:e2wwHUfEmMsa8+cuft8MT56+16EONIEK8A/gpBSco+g= k8s.io/apiserver v0.29.1/go.mod h1:V0EpkTRrJymyVT3M49we8uh2RvXf7fWC5XLB0P3SwRw= +k8s.io/cli-runtime v0.29.2 h1:smfsOcT4QujeghsNjECKN3lwyX9AwcFU0nvJ7sFN3ro= +k8s.io/cli-runtime v0.29.2/go.mod h1:KLisYYfoqeNfO+MkTWvpqIyb1wpJmmFJhioA0xd4MW8= k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= -k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw= -k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc= +k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= +k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kubectl v0.29.2 h1:uaDYaBhumvkwz0S2XHt36fK0v5IdNgL7HyUniwb2IUo= +k8s.io/kubectl v0.29.2/go.mod h1:BhizuYBGcKaHWyq+G7txGw2fXg576QbPrrnQdQDZgqI= k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= libvirt.org/go/libvirtxml v1.10000.0 h1:KPZXcmqUoJaJai95et+TEZegUQkFzCJgf1A59BOWpZQ= diff --git a/provider/cmd/app/app.go b/provider/cmd/app/app.go index b21f5155..1f80336a 100644 --- a/provider/cmd/app/app.go +++ b/provider/cmd/app/app.go @@ -54,9 +54,8 @@ import ( ) var ( - homeDir string - scheme = runtime.NewScheme() - virshExecutable string + homeDir string + scheme = runtime.NewScheme() ) func init() { @@ -85,6 +84,8 @@ type Options struct { GCVMGracefulShutdownTimeout time.Duration ResyncIntervalGarbageCollector time.Duration + + VirshExecutable string } type LibvirtOptions struct { @@ -111,7 +112,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.BaseURL, "base-url", "", "The base url to construct urls for streaming from. If empty it will be "+ "constructed from the streaming-address") - fs.StringVar(&virshExecutable, "virsh-executable", "virsh", "Path / name of the virsh executable.") + fs.StringVar(&o.VirshExecutable, "virsh-executable", "virsh", "Path / name of the virsh executable.") fs.BoolVar(&o.EnableHugepages, "enable-hugepages", false, "Enable using Hugepages.") @@ -339,7 +340,7 @@ func Run(ctx context.Context, opts Options) error { MachineClasses: machineClasses, VolumePlugins: volumePlugins, NetworkPlugins: nicPlugin, - VirshExecutable: virshExecutable, + VirshExecutable: opts.VirshExecutable, EnableHugepages: opts.EnableHugepages, }) if err != nil { diff --git a/provider/http/server_test.go b/provider/http/server_test.go index 14c2be4d..73058e62 100644 --- a/provider/http/server_test.go +++ b/provider/http/server_test.go @@ -43,5 +43,12 @@ var _ = Describe("HTTP Handler", func() { Expect(recorder.Code).To(Equal(http.StatusNotFound)) }) }) + It("should fail when http request with a not expected method called", func() { + req, err := http.NewRequest(http.MethodPut, "/exec/unknowntoken", nil) + Expect(err).NotTo(HaveOccurred()) + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + Expect(recorder.Code).To(Equal(http.StatusMethodNotAllowed)) + }) }) }) diff --git a/provider/server/exec_test.go b/provider/server/exec_test.go index 9e4d359a..3d560cdc 100644 --- a/provider/server/exec_test.go +++ b/provider/server/exec_test.go @@ -4,7 +4,12 @@ package server_test import ( + "bytes" + "context" + "fmt" + "net/http" "net/url" + "time" "github.com/digitalocean/go-libvirt" iri "github.com/ironcore-dev/ironcore/iri/apis/machine/v1alpha1" @@ -12,11 +17,14 @@ import ( libvirtutils "github.com/ironcore-dev/libvirt-provider/pkg/libvirt/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/util/httpstream/spdy" + "k8s.io/client-go/tools/remotecommand" + "k8s.io/kubectl/pkg/util/term" ) var _ = Describe("Exec", func() { - It("should return an exec-url with a token", func(ctx SpecContext) { + It("should verify an exec-url with a token", func(ctx SpecContext) { By("creating the test machine") createResp, err := machineClient.CreateMachine(ctx, &iri.CreateMachineRequest{ Machine: &iri.Machine{ @@ -30,9 +38,12 @@ var _ = Describe("Exec", func() { Expect(createResp).NotTo(BeNil()) DeferCleanup(func(ctx SpecContext) { - _, err := machineClient.DeleteMachine(ctx, &iri.DeleteMachineRequest{MachineId: createResp.Machine.Metadata.Id}) - Expect(err).ShouldNot(HaveOccurred()) Eventually(func() bool { + _, err := machineClient.DeleteMachine(ctx, &iri.DeleteMachineRequest{MachineId: createResp.Machine.Metadata.Id}) + Expect(err).To(SatisfyAny( + BeNil(), + MatchError(ContainSubstring("NotFound")), + )) _, err = libvirtConn.DomainLookupByUUID(libvirtutils.UUIDStringToBytes(createResp.Machine.Metadata.Id)) return libvirt.IsNotFound(err) }).Should(BeTrue()) @@ -55,7 +66,7 @@ var _ = Describe("Exec", func() { return libvirt.DomainState(domainState) }).Should(Equal(libvirt.DomainRunning)) - By("issuing exec for the test machine") + By("getting exec-url for the test machine") execResp, err := machineClient.Exec(ctx, &iri.ExecRequest{MachineId: createResp.Machine.Metadata.Id}) Expect(err).NotTo(HaveOccurred()) @@ -68,5 +79,69 @@ var _ = Describe("Exec", func() { Expect(parsedResUrl.Host).To(Equal(parsedBaseURL.Host)) Expect(parsedResUrl.Scheme).To(Equal(parsedBaseURL.Scheme)) Expect(parsedResUrl.Path).To(MatchRegexp(`/exec/[^/?&]{8}`)) + + By("issuing exec with response URL received and verifying tty stream") + err = runExec(ctx, parsedResUrl) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying same token cannot be used twice") + err = runExec(ctx, parsedResUrl) + Expect(err).To(MatchError(ContainSubstring("404 page not found")), "Rejecting unknown / expired token") + + By("getting exec-url with new token") + execResp, err = machineClient.Exec(ctx, &iri.ExecRequest{MachineId: createResp.Machine.Metadata.Id}) + Expect(err).NotTo(HaveOccurred()) + + By("inspecting the result") + parsedResUrl, err = url.ParseRequestURI(execResp.Url) + Expect(err).NotTo(HaveOccurred(), "url is invalid: %q", execResp.Url) + + By("deleting the machine") + _, err = machineClient.DeleteMachine(ctx, &iri.DeleteMachineRequest{MachineId: createResp.Machine.Metadata.Id}) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(func() bool { + _, err = libvirtConn.DomainLookupByUUID(libvirtutils.UUIDStringToBytes(createResp.Machine.Metadata.Id)) + return libvirt.IsNotFound(err) + }).Should(BeTrue()) + + By("verifying exec url with a valid token and a not existing machine fails") + err = runExec(ctx, parsedResUrl) + machineNotSynchErr := fmt.Sprintf("machine %s has not yet been synced", createResp.Machine.Metadata.Id) + Expect(err).To(SatisfyAny(MatchError(ContainSubstring("404 page not found")), MatchError(ContainSubstring(machineNotSynchErr)))) + }) }) + +func runExec(ctx context.Context, execUrl *url.URL) error { + randomSize := 1024 * 1024 + randomData := make([]byte, randomSize) + var stdout bytes.Buffer + + tty := term.TTY{ + In: bytes.NewReader(randomData), + Out: &stdout, + Raw: true, + TryDev: true, + } + + roundTripper, err := spdy.NewRoundTripperWithConfig(spdy.RoundTripperConfig{ + TLS: http.DefaultTransport.(*http.Transport).TLSClientConfig, + Proxier: http.ProxyFromEnvironment, + PingPeriod: 5 * time.Second, + }) + if err != nil { + return err + } + exec, err := remotecommand.NewSPDYExecutorForTransports(roundTripper, roundTripper, http.MethodGet, execUrl) + if err != nil { + return err + } + + return tty.Safe(func() error { + return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdin: tty.In, + Stdout: tty.Out, + Tty: true, + }) + }) +} diff --git a/provider/server/server_suite_test.go b/provider/server/server_suite_test.go index cdb2d66f..004df75d 100644 --- a/provider/server/server_suite_test.go +++ b/provider/server/server_suite_test.go @@ -33,7 +33,7 @@ const ( consistentlyDuration = 1 * time.Second machineClassx3xlarge = "x3-xlarge" machineClassx2medium = "x2-medium" - baseURL = "http://localhost:8080" + baseURL = "http://localhost:20251" streamingAddress = "127.0.0.1:20251" ) @@ -110,6 +110,7 @@ var _ = BeforeSuite(func() { GCVMGracefulShutdownTimeout: 10 * time.Second, ResyncIntervalGarbageCollector: 5 * time.Second, ResyncIntervalVolumeSize: 1 * time.Minute, + VirshExecutable: "virsh", } srvCtx, cancel := context.WithCancel(context.Background())