diff --git a/src/cli/README.md b/src/cli/README.md new file mode 100644 index 0000000000..475330b49c --- /dev/null +++ b/src/cli/README.md @@ -0,0 +1,61 @@ +# Virtualization +Subcommand for the command line client for Deckhouse. +Manages virtual machine-related operations in your Kubernetes cluster. + +### Available Commands: +* console - Connect to a console of a virtual machine. +* port-forward - Forward local ports to a virtual machine +* scp - SCP files from/to a virtual machine. +* ssh - Open an ssh connection to a virtual machine. +* vnc - Open a vnc connection to a virtual machine. +* start - Start a virtual machine. +* stop - Stop a virtual machine. +* restart - Restart a virtual machine. +* evict - Evict a virtual machine. + +### Examples +#### console +```shell +d8 virtualization console myvm +d8 virtualization console myvm.mynamespace +``` +#### port-forward +```shell +d8 virtualization port-forward myvm tcp/8080:8080 +d8 virtualization port-forward --stdio=true myvm.mynamespace 22 +``` +#### scp +```shell +d8 virtualization scp myfile.bin user@myvm:myfile.bin +d8 virtualization scp user@myvm:myfile.bin ~/myfile.bin +``` +#### ssh +```shell +d8 virtualization --identity-file=/path/to/ssh_key ssh user@myvm.mynamespace +d8 virtualization ssh --local-ssh=true --namespace=mynamespace --username=user myvm +``` +#### vnc +```shell +d8 virtualization vnc myvm.mynamespace +d8 virtualization vnc myvm -n mynamespace +``` +#### start +```shell +d8 virtualization start myvm.mynamespace --wait +d8 virtualization start myvm -n mynamespace +``` +#### stop +```shell +d8 virtualization stop myvm.mynamespace --force +d8 virtualization stop myvm -n mynamespace +``` +#### restart +```shell +d8 virtualization restart myvm.mynamespace --timeout=1m +d8 virtualization restart myvm -n mynamespace +``` +#### evict +```shell +d8 virtualization evict myvm.mynamespace +d8 virtualization evict myvm -n mynamespace +``` \ No newline at end of file diff --git a/src/cli/Taskfile.yaml b/src/cli/Taskfile.yaml new file mode 100644 index 0000000000..ea2ed17594 --- /dev/null +++ b/src/cli/Taskfile.yaml @@ -0,0 +1,21 @@ +# https://taskfile.dev + +version: "3" + +silent: true + +tasks: + build: + cmds: + - go build -o out/d8v cmd/main.go + install: + deps: [build] + cmds: + - echo "Check that ~/.local/bin in your PATH" + - echo "Installing d8v to ~/.local/bin" + - mkdir -p ~/.local/bin + - cp out/d8v ~/.local/bin/d8v + - task: clean + clean: + cmds: + - rm -rf out diff --git a/src/cli/cmd/main.go b/src/cli/cmd/main.go new file mode 100644 index 0000000000..cb8803ab18 --- /dev/null +++ b/src/cli/cmd/main.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package main + +import ( + "os" + + "github.com/deckhouse/virtualization/src/cli/pkg/command" +) + +func main() { + virtCmd, _ := command.NewCommand(os.Args[0]) + if err := virtCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/src/cli/go.mod b/src/cli/go.mod new file mode 100644 index 0000000000..0b002281cc --- /dev/null +++ b/src/cli/go.mod @@ -0,0 +1,74 @@ +module github.com/deckhouse/virtualization/src/cli + +go 1.23.6 + +toolchain go1.24.0 + +require ( + github.com/deckhouse/deckhouse-cli v0.12.1 + github.com/deckhouse/virtualization/api v0.15.0 + github.com/gorilla/websocket v1.5.3 + github.com/povsister/scp v0.0.0-20250504051308-e467f71ea63c + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + golang.org/x/crypto v0.33.0 + golang.org/x/sys v0.30.0 + golang.org/x/term v0.29.0 + golang.org/x/text v0.22.0 + k8s.io/apimachinery v0.32.2 + k8s.io/client-go v0.32.2 + k8s.io/component-base v0.29.3 + k8s.io/klog/v2 v2.130.1 +) + +require github.com/golang/protobuf v1.5.4 // indirect + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.2 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 // indirect + github.com/openshift/custom-resource-status v1.1.2 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.2 // indirect + k8s.io/apiextensions-apiserver v0.29.3 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect + kubevirt.io/api v1.2.0 // indirect + kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 // indirect + kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/src/cli/go.sum b/src/cli/go.sum new file mode 100644 index 0000000000..6dd4e59c65 --- /dev/null +++ b/src/cli/go.sum @@ -0,0 +1,415 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckhouse/deckhouse-cli v0.12.1 h1:wKnDwx1oQdMLtJTisKst/gvswQxBPKK+CzY4awdzZt4= +github.com/deckhouse/deckhouse-cli v0.12.1/go.mod h1:Hhx+Ov3WZCdu+GpBQzg7rplh1IUP+fGyNGCudNYbMjo= +github.com/deckhouse/virtualization/api v0.15.0 h1:yRX6n18kK9wwO2f+8fc2s2nb1N2vYKxKb3/aSRtc9Kk= +github.com/deckhouse/virtualization/api v0.15.0/go.mod h1:t+6i4NC43RfNLqcZqkEc5vxY1ypKceqmOOKlVEq0cYA= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= +github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 h1:t/CahSnpqY46sQR01SoS+Jt0jtjgmhgE6lFmRnO4q70= +github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183/go.mod h1:4VWG+W22wrB4HfBL88P40DxLEpSOaiBVxUnfalfJo9k= +github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4= +github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/povsister/scp v0.0.0-20250504051308-e467f71ea63c h1:1+j5JHz9mUzYSp0scuF6hzvJP28EDBFe5eBJb0xnGk4= +github.com/povsister/scp v0.0.0-20250504051308-e467f71ea63c/go.mod h1:CiJNEeV6v0tUCNul/+gTjl+FgjfImoiuptJB9AEzqjE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= +k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= +k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= +k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI= +k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc= +k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= +k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= +k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo= +k8s.io/component-base v0.29.3/go.mod h1:Yuj33XXjuOk2BAaHsIGHhCKZQAgYKhqIxIjIr2UXYio= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +kubevirt.io/api v1.2.0 h1:1f8XQLPl4BuHPsc6SHTPnYSYeDxucKCQGa8CdrGJSRc= +kubevirt.io/api v1.2.0/go.mod h1:SbeR9ma4EwnaOZEUkh/lNz0kzYm5LPpEDE30vKXC5Zg= +kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 h1:IWo12+ei3jltSN5jQN1xjgakfvRSF3G3Rr4GXVOOy2I= +kubevirt.io/containerized-data-importer-api v1.57.0-alpha1/go.mod h1:Y/8ETgHS1GjO89bl682DPtQOYEU/1ctPFBz6Sjxm4DM= +kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 h1:QMrd0nKP0BGbnxTqakhDZAUhGKxPiPiN5gSDqKUmGGc= +kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/src/cli/internal/cmd/console/console.go b/src/cli/internal/cmd/console/console.go new file mode 100644 index 0000000000..096effe6f1 --- /dev/null +++ b/src/cli/internal/cmd/console/console.go @@ -0,0 +1,149 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/console/console.go +*/ + +package console + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/src/cli/internal/templates" + "github.com/deckhouse/virtualization/src/cli/internal/util" +) + +var timeout int + +func NewCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + cmd := &cobra.Command{ + Use: "console (VirtualMachine)", + Short: "Connect to a console of a virtual machine.", + Example: usage(), + Args: templates.ExactArgs("console", 1), + RunE: func(cmd *cobra.Command, args []string) error { + c := Console{clientConfig: clientConfig} + return c.Run(args) + }, + } + + cmd.Flags().IntVar(&timeout, "timeout", 5, "The number of minutes to wait for the virtual machine to be ready.") + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} + +type Console struct { + clientConfig clientcmd.ClientConfig +} + +func usage() string { + usage := ` # Connect to the console on VirtualMachine 'myvm': + {{ProgramName}} console myvm + {{ProgramName}} console myvm.mynamespace + {{ProgramName}} console myvm -n mynamespace + # Configure one minute timeout (default 5 minutes) + {{ProgramName}} console --timeout=1 myvm` + + return usage +} + +func (c *Console) Run(args []string) error { + namespace, name, err := templates.ParseTarget(args[0]) + if err != nil { + return err + } + if namespace == "" { + namespace, _, err = c.clientConfig.Namespace() + if err != nil { + return err + } + } + + virtCli, err := kubeclient.GetClientFromClientConfig(c.clientConfig) + if err != nil { + return err + } + + for { + err := connect(name, namespace, virtCli) + if err != nil { + if errors.Is(err, util.ErrorInterrupt) || strings.Contains(err.Error(), "not found") { + return err + } + + var e *websocket.CloseError + if errors.As(err, &e) { + switch e.Code { + case websocket.CloseGoingAway: + fmt.Fprint(os.Stderr, "\nYou were disconnected from the console. This has one of the following reasons:"+ + "\n - another user connected to the console of the target vm\n") + return nil + case websocket.CloseAbnormalClosure: + fmt.Fprint(os.Stderr, "\nYou were disconnected from the console. This has one of the following reasons:"+ + "\n - network issues"+ + "\n - machine restart\n") + } + } else { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + + time.Sleep(time.Second) + } + } +} + +func connect(name string, namespace string, virtCli kubeclient.Client) error { + stdinReader, stdinWriter := io.Pipe() + stdoutReader, stdoutWriter := io.Pipe() + + // in -> stdinWriter | stdinReader -> console + // out <- stdoutReader | stdoutWriter <- console + // Wait until the virtual machine is in running phase, user interrupt or timeout + resChan := make(chan error) + runningChan := make(chan error) + + go func() { + con, err := virtCli.VirtualMachines(namespace).SerialConsole(name, &kubeclient.SerialConsoleOptions{ConnectionTimeout: time.Duration(timeout) * time.Minute}) + runningChan <- err + + if err != nil { + return + } + + resChan <- con.Stream(kubeclient.StreamOptions{ + In: stdinReader, + Out: stdoutWriter, + }) + }() + + err := <-runningChan + if err != nil { + return err + } + + err = util.AttachConsole(stdinReader, stdoutReader, stdinWriter, stdoutWriter, name, resChan) + return err +} diff --git a/src/cli/internal/cmd/lifecycle/evict.go b/src/cli/internal/cmd/lifecycle/evict.go new file mode 100644 index 0000000000..f54417c9b4 --- /dev/null +++ b/src/cli/internal/cmd/lifecycle/evict.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package lifecycle + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func NewEvictCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + lifecycle := NewLifecycle(Evict, clientConfig) + cmd := &cobra.Command{ + Use: "evict (VirtualMachine)", + Short: "Evict a virtual machine.", + Example: lifecycle.Usage(), + Args: templates.ExactArgs("evict", 1), + RunE: func(cmd *cobra.Command, args []string) error { + return lifecycle.Run(args) + }, + } + AddCommandlineArgs(cmd.Flags(), &lifecycle.opts) + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} diff --git a/src/cli/internal/cmd/lifecycle/lifecycle.go b/src/cli/internal/cmd/lifecycle/lifecycle.go new file mode 100644 index 0000000000..047998e508 --- /dev/null +++ b/src/cli/internal/cmd/lifecycle/lifecycle.go @@ -0,0 +1,183 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package lifecycle + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/spf13/pflag" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + + "github.com/deckhouse/virtualization/src/cli/internal/cmd/lifecycle/vmop" + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +type Command string + +const ( + Stop Command = "stop" + Start Command = "start" + Restart Command = "restart" + Evict Command = "evict" +) + +type Manager interface { + Stop(ctx context.Context, name, namespace string) (msg string, err error) + Start(ctx context.Context, name, namespace string) (msg string, err error) + Restart(ctx context.Context, name, namespace string) (msg string, err error) + Evict(ctx context.Context, name, namespace string) (msg string, err error) +} + +func NewLifecycle(cmd Command, clientConfig clientcmd.ClientConfig) *Lifecycle { + return &Lifecycle{ + cmd: cmd, + clientConfig: clientConfig, + opts: DefaultOptions(), + } +} + +type Lifecycle struct { + cmd Command + clientConfig clientcmd.ClientConfig + opts Options +} + +func DefaultOptions() Options { + return Options{ + Force: false, + WaitComplete: false, + CreateOnly: false, + Timeout: 5 * time.Minute, + } +} + +type Options struct { + Force bool + WaitComplete bool + CreateOnly bool + Timeout time.Duration +} + +func (l *Lifecycle) Run(args []string) error { + name, namespace, err := l.getNameNamespace(args) + key := types.NamespacedName{Namespace: namespace, Name: name} + if err != nil { + return err + } + mgr, err := l.getManager() + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), l.opts.Timeout) + defer cancel() + writer := os.Stdout + var msg string + switch l.cmd { + case Stop: + fmt.Fprintf(writer, "Stopping virtual machine %q\n", key.String()) + msg, err = mgr.Stop(ctx, name, namespace) + case Start: + fmt.Fprintf(writer, "Starting virtual machine %q\n", key.String()) + msg, err = mgr.Start(ctx, name, namespace) + case Restart: + fmt.Fprintf(writer, "Restarting virtual machine %q\n", key.String()) + msg, err = mgr.Restart(ctx, name, namespace) + case Evict: + fmt.Fprintf(writer, "Evicting virtual machine %q\n", key.String()) + msg, err = mgr.Evict(ctx, name, namespace) + default: + return fmt.Errorf("invalid command %q", l.cmd) + } + if msg != "" { + fmt.Fprint(os.Stdout, msg) + } + return err +} + +func (l *Lifecycle) Usage() string { + opts := DefaultOptions() + usage := fmt.Sprintf(` # %s VirtualMachine 'myvm':`, cases.Title(language.English).String(string(l.cmd))) + usage += strings.Replace(fmt.Sprintf(` + {{ProgramName}} {{operation}} myvm + {{ProgramName}} {{operation}} myvm.mynamespace + {{ProgramName}} {{operation}} myvm -n mynamespace + # Configure one minute timeout (default: timeout=%v) + {{ProgramName}} {{operation}} --%s=1m myvm + # Configure wait vm phase (default: wait=%v) + {{ProgramName}} {{operation}} --%s myvm`, opts.Timeout, timeoutFlag, opts.WaitComplete, waitFlag), "{{operation}}", string(l.cmd), -1) + if l.cmd != Start && l.cmd != Evict { + usage += fmt.Sprintf(` + # Configure shutdown policy (default: force=%v) + {{ProgramName}} %s --%s myvm`, opts.Force, l.cmd, forceFlag) + } + return usage +} + +func (l *Lifecycle) getNameNamespace(args []string) (string, string, error) { + namespace, name, err := templates.ParseTarget(args[0]) + if err != nil { + return "", "", err + } + if namespace == "" { + namespace, _, err = l.clientConfig.Namespace() + if err != nil { + return "", "", err + } + } + return name, namespace, nil +} + +func (l *Lifecycle) getManager() (Manager, error) { + virtCli, err := kubeclient.GetClientFromClientConfig(l.clientConfig) + if err != nil { + return nil, err + } + + return vmop.New( + virtCli, + vmop.WithCreateOnly(l.opts.CreateOnly), + vmop.WithWaitComplete(l.opts.WaitComplete), + vmop.WithForce(l.opts.Force), + ), nil +} + +const ( + forceFlag, forceFlagShort = "force", "f" + waitFlag, waitFlagShort = "wait", "w" + createOnlyFlag, createOnlyFlagShort = "create-only", "c" + timeoutFlag, timeoutFlagShort = "timeout", "t" +) + +func AddCommandlineArgs(flagset *pflag.FlagSet, opts *Options) { + flagset.BoolVarP(&opts.Force, forceFlag, forceFlagShort, opts.Force, + fmt.Sprintf("--%s, -%s: Set this flag to force the operation.", forceFlag, forceFlagShort)) + flagset.BoolVarP(&opts.WaitComplete, waitFlag, waitFlagShort, opts.WaitComplete, + fmt.Sprintf("--%s, -%s: Set this flag to wait for the operation to complete.", waitFlag, waitFlagShort)) + flagset.BoolVarP(&opts.CreateOnly, createOnlyFlag, createOnlyFlagShort, opts.CreateOnly, + fmt.Sprintf("--%s, -%s: Set this flag for create operation only.", createOnlyFlag, createOnlyFlagShort)) + flagset.DurationVarP(&opts.Timeout, timeoutFlag, timeoutFlagShort, opts.Timeout, + fmt.Sprintf("--%s, -%s: Set this flag to change the timeout.", timeoutFlag, timeoutFlagShort)) +} diff --git a/src/cli/internal/cmd/lifecycle/restart.go b/src/cli/internal/cmd/lifecycle/restart.go new file mode 100644 index 0000000000..4b1a690f4f --- /dev/null +++ b/src/cli/internal/cmd/lifecycle/restart.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package lifecycle + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func NewRestartCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + lifecycle := NewLifecycle(Restart, clientConfig) + cmd := &cobra.Command{ + Use: "restart (VirtualMachine)", + Short: "Restart a virtual machine.", + Example: lifecycle.Usage(), + Args: templates.ExactArgs("restart", 1), + RunE: func(cmd *cobra.Command, args []string) error { + return lifecycle.Run(args) + }, + } + AddCommandlineArgs(cmd.Flags(), &lifecycle.opts) + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} diff --git a/src/cli/internal/cmd/lifecycle/start.go b/src/cli/internal/cmd/lifecycle/start.go new file mode 100644 index 0000000000..900feb0f00 --- /dev/null +++ b/src/cli/internal/cmd/lifecycle/start.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package lifecycle + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func NewStartCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + lifecycle := NewLifecycle(Start, clientConfig) + cmd := &cobra.Command{ + Use: "start (VirtualMachine)", + Short: "Start a virtual machine.", + Example: lifecycle.Usage(), + Args: templates.ExactArgs("start", 1), + RunE: func(cmd *cobra.Command, args []string) error { + return lifecycle.Run(args) + }, + } + AddCommandlineArgs(cmd.Flags(), &lifecycle.opts) + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} diff --git a/src/cli/internal/cmd/lifecycle/stop.go b/src/cli/internal/cmd/lifecycle/stop.go new file mode 100644 index 0000000000..5ce43ed782 --- /dev/null +++ b/src/cli/internal/cmd/lifecycle/stop.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package lifecycle + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func NewStopCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + lifecycle := NewLifecycle(Stop, clientConfig) + cmd := &cobra.Command{ + Use: "stop (VirtualMachine)", + Short: "Stop a virtual machine.", + Example: lifecycle.Usage(), + Args: templates.ExactArgs("stop", 1), + RunE: func(cmd *cobra.Command, args []string) error { + return lifecycle.Run(args) + }, + } + AddCommandlineArgs(cmd.Flags(), &lifecycle.opts) + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} diff --git a/src/cli/internal/cmd/lifecycle/vmop/vmop.go b/src/cli/internal/cmd/lifecycle/vmop/vmop.go new file mode 100644 index 0000000000..e41ea75276 --- /dev/null +++ b/src/cli/internal/cmd/lifecycle/vmop/vmop.go @@ -0,0 +1,249 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package vmop + +import ( + "context" + "fmt" + "strings" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmopcondition" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" +) + +type VirtualMachineOperation struct { + client kubeclient.Client + options options +} + +type options struct { + force bool + waitComplete bool + createOnly bool +} + +func New(client kubeclient.Client, opts ...func(*VirtualMachineOperation)) *VirtualMachineOperation { + vmop := &VirtualMachineOperation{ + client: client, + options: options{}, + } + + for _, opt := range opts { + opt(vmop) + } + + return vmop +} + +func WithForce(force bool) func(*VirtualMachineOperation) { + return func(o *VirtualMachineOperation) { + o.options.force = force + } +} + +func WithWaitComplete(waitComplete bool) func(*VirtualMachineOperation) { + return func(o *VirtualMachineOperation) { + o.options.waitComplete = waitComplete + } +} + +func WithCreateOnly(createOnly bool) func(*VirtualMachineOperation) { + return func(o *VirtualMachineOperation) { + o.options.createOnly = createOnly + } +} + +func (v VirtualMachineOperation) Stop(ctx context.Context, vmName, vmNamespace string) (msg string, err error) { + vmop := v.newVMOP(vmName, vmNamespace, v1alpha2.VMOPTypeStop, false) + return v.do(ctx, vmop, v.options.createOnly, v.options.waitComplete) +} + +func (v VirtualMachineOperation) Start(ctx context.Context, vmName, vmNamespace string) (msg string, err error) { + vmop := v.newVMOP(vmName, vmNamespace, v1alpha2.VMOPTypeStart, false) + return v.do(ctx, vmop, v.options.createOnly, v.options.waitComplete) +} + +func (v VirtualMachineOperation) Restart(ctx context.Context, vmName, vmNamespace string) (msg string, err error) { + vmop := v.newVMOP(vmName, vmNamespace, v1alpha2.VMOPTypeRestart, v.options.force) + return v.do(ctx, vmop, v.options.createOnly, v.options.waitComplete) +} + +func (v VirtualMachineOperation) Evict(ctx context.Context, vmName, vmNamespace string) (msg string, err error) { + vmop := v.newVMOP(vmName, vmNamespace, v1alpha2.VMOPTypeEvict, v.options.force) + return v.do(ctx, vmop, v.options.createOnly, v.options.waitComplete) +} + +func (v VirtualMachineOperation) do(ctx context.Context, vmop *v1alpha2.VirtualMachineOperation, createOnly, waitCompleted bool) (msg string, err error) { + if createOnly { + vmop, err = v.create(ctx, vmop) + } else { + vmop, err = v.createAndWait(ctx, vmop, waitCompleted) + } + msg = v.generateMsg(vmop) + return msg, err +} + +func (v VirtualMachineOperation) generateMsg(vmop *v1alpha2.VirtualMachineOperation) string { + if vmop == nil { + return "" + } + key := types.NamespacedName{Namespace: vmop.GetNamespace(), Name: vmop.GetName()} + vmKey := types.NamespacedName{Namespace: vmop.GetNamespace(), Name: vmop.Spec.VirtualMachine} + phase := vmop.Status.Phase + + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("VirtualMachine %q ", vmKey.String())) + + if v.isPhaseOrFailed(vmop, v1alpha2.VMOPPhaseCompleted) { + if !v.isCompleted(vmop) { + sb.WriteString("was not ") + } + switch vmop.Spec.Type { + case v1alpha2.VMOPTypeStart: + sb.WriteString("started. ") + case v1alpha2.VMOPTypeStop: + sb.WriteString("stopped. ") + case v1alpha2.VMOPTypeRestart: + sb.WriteString("restarted. ") + case v1alpha2.VMOPTypeEvict: + sb.WriteString("evicted.") + } + } else { + switch vmop.Spec.Type { + case v1alpha2.VMOPTypeStart: + sb.WriteString("starting. ") + case v1alpha2.VMOPTypeStop: + sb.WriteString("stopping. ") + case v1alpha2.VMOPTypeRestart: + sb.WriteString("restarting. ") + case v1alpha2.VMOPTypeEvict: + sb.WriteString("evicting.") + } + } + + sb.WriteString(fmt.Sprintf("VirtualMachineOperation %q ", key.String())) + switch phase { + case v1alpha2.VMOPPhasePending: + sb.WriteString("pending.") + case v1alpha2.VMOPPhaseInProgress: + sb.WriteString("in progress.") + case v1alpha2.VMOPPhaseCompleted: + sb.WriteString("completed.") + case v1alpha2.VMOPPhaseFailed: + cond, _ := getCondition(vmopcondition.TypeCompleted.String(), vmop.Status.Conditions) + sb.WriteString(fmt.Sprintf("failed. type=%q reason=%q, message=%q.", cond.Type, cond.Reason, cond.Message)) + case "": + sb.WriteString("created.") + default: + sb.WriteString(fmt.Sprintf(" phase=%q.", phase)) + } + sb.WriteString("\n") + return sb.String() +} + +func (v VirtualMachineOperation) createAndWait(ctx context.Context, vmop *v1alpha2.VirtualMachineOperation, waitCompleted bool) (*v1alpha2.VirtualMachineOperation, error) { + vmop, err := v.create(ctx, vmop) + if err != nil { + return nil, err + } + if v.isPhaseOrFailed(vmop, v1alpha2.VMOPPhaseCompleted) { + return vmop, nil + } + + if waitCompleted { + return v.waitUntil(ctx, vmop.GetName(), vmop.GetNamespace(), v1alpha2.VMOPPhaseCompleted) + } + + return v.waitUntil(ctx, vmop.GetName(), vmop.GetNamespace(), v1alpha2.VMOPPhaseInProgress) +} + +func (v VirtualMachineOperation) create(ctx context.Context, vmop *v1alpha2.VirtualMachineOperation) (*v1alpha2.VirtualMachineOperation, error) { + return v.client.VirtualMachineOperations(vmop.GetNamespace()).Create(ctx, vmop, metav1.CreateOptions{}) +} + +func (v VirtualMachineOperation) waitUntil(ctx context.Context, name, namespace string, phase v1alpha2.VMOPPhase) (*v1alpha2.VirtualMachineOperation, error) { + var vmop *v1alpha2.VirtualMachineOperation + selector, err := fields.ParseSelector(fmt.Sprintf("metadata.name=%s", name)) + if err != nil { + return nil, err + } + watcher, err := v.client.VirtualMachineOperations(namespace).Watch(ctx, metav1.ListOptions{FieldSelector: selector.String()}) + if err != nil { + return nil, err + } + defer watcher.Stop() + for event := range watcher.ResultChan() { + op, ok := event.Object.(*v1alpha2.VirtualMachineOperation) + if !ok { + continue + } + if v.isPhaseOrFailed(op, phase) { + vmop = op + break + } + } + if !v.isPhaseOrFailed(vmop, phase) { + return nil, context.DeadlineExceeded + } + return vmop, nil +} + +func (v VirtualMachineOperation) isCompleted(vmop *v1alpha2.VirtualMachineOperation) bool { + if vmop == nil { + return false + } + return vmop.Status.Phase == v1alpha2.VMOPPhaseCompleted +} + +func (v VirtualMachineOperation) isPhaseOrFailed(vmop *v1alpha2.VirtualMachineOperation, phase v1alpha2.VMOPPhase) bool { + if vmop == nil { + return false + } + return vmop.Status.Phase == phase || vmop.Status.Phase == v1alpha2.VMOPPhaseFailed +} + +func (v VirtualMachineOperation) newVMOP(vmName, vmNamespace string, t v1alpha2.VMOPType, force bool) *v1alpha2.VirtualMachineOperation { + return &v1alpha2.VirtualMachineOperation{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha2.VirtualMachineOperationKind, + APIVersion: v1alpha2.Version, + }, + ObjectMeta: metav1.ObjectMeta{ + GenerateName: vmName + "-", + Namespace: vmNamespace, + }, + Spec: v1alpha2.VirtualMachineOperationSpec{ + Type: t, + VirtualMachine: vmName, + Force: force, + }, + } +} + +func getCondition(condType string, conds []metav1.Condition) (metav1.Condition, bool) { + for _, cond := range conds { + if cond.Type == condType { + return cond, true + } + } + + return metav1.Condition{}, false +} diff --git a/src/cli/internal/cmd/portforward/portforward.go b/src/cli/internal/cmd/portforward/portforward.go new file mode 100644 index 0000000000..facbda2a5e --- /dev/null +++ b/src/cli/internal/cmd/portforward/portforward.go @@ -0,0 +1,219 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/portforward/portforward.go +*/ + +package portforward + +import ( + "errors" + "fmt" + "net" + "os" + "os/signal" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "k8s.io/klog/v2" + + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/api/subresources/v1alpha2" + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +const ( + forwardToStdioFlag = "stdio" + addressFlag = "address" +) + +var ( + forwardToStdio bool + address = "127.0.0.1" +) + +func NewCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + cmd := &cobra.Command{ + Use: "port-forward name[.namespace] [protocol/]localPort[:targetPort]...", + Short: "Forward local ports to a virtual machine", + Long: usage(), + Example: examples(), + Args: func(cmd *cobra.Command, args []string) error { + if n := len(args); n < 2 { + klog.Errorf("fatal: Number of input parameters is incorrect, portforward requires at least 2 arg(s), received %d", n) + // always write to stderr on failures to ensure they get printed in stdio mode + cmd.SetOut(os.Stderr) + err := cmd.Help() + if err != nil { + return err + } + return errors.New("argument validation failed") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + c := PortForward{clientConfig: clientConfig} + return c.Run(cmd, args) + }, + } + cmd.Flags().BoolVar(&forwardToStdio, forwardToStdioFlag, forwardToStdio, + fmt.Sprintf("--%s=true: Set this to true to forward the tunnel to stdout/stdin; Only works with a single port", forwardToStdioFlag)) + cmd.Flags().StringVar(&address, addressFlag, address, + fmt.Sprintf("--%s=: Set this to the address the local ports should be opened on", addressFlag)) + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} + +type PortForward struct { + address *net.IPAddr + clientConfig clientcmd.ClientConfig + resource portforwardableResource +} + +func (o *PortForward) Run(cmd *cobra.Command, args []string) error { + setOutput(cmd) + namespace, name, ports, err := o.prepareCommand(args) + if err != nil { + return err + } + + if err := o.setResource(namespace); err != nil { + return err + } + + if forwardToStdio { + if len(ports) != 1 { + return errors.New("only one port supported when forwarding to stdout") + } + return o.startStdoutStream(namespace, name, ports[0]) + } + + o.address, err = net.ResolveIPAddr("", address) + if err != nil { + return err + } + + if err := o.startPortForwards(namespace, name, ports); err != nil { + return err + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + return nil +} + +func (o *PortForward) prepareCommand(args []string) (namespace, name string, ports []forwardedPort, err error) { + namespace, name, err = templates.ParseTarget(args[0]) + if err != nil { + return + } + + ports, err = parsePorts(args[1:]) + if err != nil { + return + } + + if len(namespace) < 1 { + namespace, _, err = o.clientConfig.Namespace() + if err != nil { + return + } + } + + return +} + +func (o *PortForward) setResource(namespace string) error { + client, err := kubeclient.GetClientFromClientConfig(o.clientConfig) + if err != nil { + return err + } + o.resource = client.VirtualMachines(namespace) + return nil +} + +func (o *PortForward) startStdoutStream(namespace, name string, port forwardedPort) error { + streamer, err := o.resource.PortForward(name, v1alpha2.VirtualMachinePortForward{Port: port.remote, Protocol: port.protocol}) + if err != nil { + return err + } + + klog.V(3).Infof("forwarding to %s/%s:%d", namespace, name, port.remote) + if err := streamer.Stream(kubeclient.StreamOptions{ + In: os.Stdin, + Out: os.Stdout, + }); err != nil { + return err + } + + return nil +} + +func (o *PortForward) startPortForwards(namespace, name string, ports []forwardedPort) error { + for _, port := range ports { + forwarder := portForwarder{ + namespace: namespace, + name: name, + resource: o.resource, + } + if err := forwarder.startForwarding(o.address, port); err != nil { + return err + } + } + return nil +} + +// setOutput to stderr if we're using stdout for traffic +func setOutput(cmd *cobra.Command) { + if forwardToStdio { + cmd.SetOut(os.Stderr) + cmd.Root().SetOut(os.Stderr) + } else { + cmd.SetOut(os.Stdout) + } +} + +func usage() string { + return `Forward local ports to a virtualmachine. +The port argument supports the syntax protocol/localPort:targetPort with protocol/ and :targetPort as optional fields. +Protocol supports TCP (default) and UDP. + +Portforwards get established over the Kubernetes control-plane using websocket streams. +Usage can be restricted by the cluster administrator through the /portforward subresource. +` +} + +func examples() string { + return ` #Forward the local port 8080 to the vm port + {{ProgramName}} port-forward myvm 8080 + + # Forward the local port 8080 to the vm port in mynamespace + {{ProgramName}} port-forward myvm.mynamespace 8080 + {{ProgramName}} port-forward myvm 8080 -n mynamespace + + # Note: {{ProgramName}} port-forward sends all traffic over the Kubernetes API Server. + # This means any traffic will add additional pressure to the control plane. + # For continuous traffic intensive connections, consider using a dedicated Kubernetes Service. + + # Open an SSH connection using PortForward and ProxyCommand: + ssh -o 'ProxyCommand={{ProgramName}} port-forward --stdio=true myvm.mynamespace 22' user@myvm.mynamespace + + # Use as SCP ProxyCommand: + scp -o 'ProxyCommand={{ProgramName}} port-forward --stdio=true myvm.mynamespace 22' local.file user@myvm.mynamespace` +} diff --git a/src/cli/internal/cmd/portforward/portforwarder.go b/src/cli/internal/cmd/portforward/portforwarder.go new file mode 100644 index 0000000000..3be6298236 --- /dev/null +++ b/src/cli/internal/cmd/portforward/portforwarder.go @@ -0,0 +1,60 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/portforward/portforwarder.go +*/ + +package portforward + +import ( + "errors" + "net" + "strings" + + "k8s.io/klog/v2" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/api/subresources/v1alpha2" +) + +type portForwarder struct { + namespace string + name string + resource portforwardableResource +} + +type portforwardableResource interface { + PortForward(name string, options v1alpha2.VirtualMachinePortForward) (kubeclient.StreamInterface, error) +} + +func (p *portForwarder) startForwarding(address *net.IPAddr, port forwardedPort) error { + klog.Infof("forwarding %s %s:%d to %d", port.protocol, address, port.local, port.remote) + if port.protocol == protocolUDP { + return p.startForwardingUDP(address, port) + } + + if port.protocol == protocolTCP { + return p.startForwardingTCP(address, port) + } + + return errors.New("unknown protocol: " + port.protocol) +} + +func handleConnectionError(err error, port forwardedPort) { + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + klog.Errorf("error handling connection for %d: %v", port.local, err) + } +} diff --git a/src/cli/internal/cmd/portforward/ports.go b/src/cli/internal/cmd/portforward/ports.go new file mode 100644 index 0000000000..df9634f212 --- /dev/null +++ b/src/cli/internal/cmd/portforward/ports.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/portforward/ports.go +*/ + +package portforward + +import ( + "errors" + "strconv" + "strings" +) + +type forwardedPort struct { + local int + remote int + protocol string +} + +func parsePorts(args []string) ([]forwardedPort, error) { + ports := make([]forwardedPort, len(args)) + + for i, arg := range args { + forwardedPort, err := parsePort(arg) + if err != nil { + return ports, err + } + ports[i] = forwardedPort + } + + return ports, nil +} + +const ( + protocolTCP = "tcp" + protocolUDP = "udp" +) + +func parsePort(arg string) (forwardedPort, error) { + var ( + port = forwardedPort{ + // default to tcp + protocol: protocolTCP, + } + err error + ) + + protocol := strings.Split(arg, "/") + if len(protocol) > 1 { + port.protocol = protocol[0] + arg = protocol[1] + } + + ports := strings.FieldsFunc(arg, func(r rune) bool { + return r == ':' + }) + if len(ports) < 1 { + return port, errors.New("invalid port, missing local and/or remote port") + } + + port.local, err = strconv.Atoi(ports[0]) + if err != nil { + return port, err + } + port.remote = port.local + + if len(ports) > 1 { + port.remote, err = strconv.Atoi(ports[1]) + if err != nil { + return port, err + } + } + + return port, nil +} diff --git a/src/cli/internal/cmd/portforward/tcp.go b/src/cli/internal/cmd/portforward/tcp.go new file mode 100644 index 0000000000..736c9f89f8 --- /dev/null +++ b/src/cli/internal/cmd/portforward/tcp.go @@ -0,0 +1,83 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/portforward/tcp.go +*/ + +package portforward + +import ( + "io" + "net" + + "k8s.io/klog/v2" + + "github.com/deckhouse/virtualization/api/subresources/v1alpha2" +) + +func (p *portForwarder) startForwardingTCP(address *net.IPAddr, port forwardedPort) error { + listener, err := net.ListenTCP( + port.protocol, + &net.TCPAddr{ + IP: address.IP, + Zone: address.Zone, + Port: port.local, + }) + if err != nil { + return err + } + + go p.waitForConnection(listener, port) + + return nil +} + +func (p *portForwarder) waitForConnection(listener net.Listener, port forwardedPort) { + for { + conn, err := listener.Accept() + if err != nil { + klog.Errorln("error accepting connection:", err) + return + } + klog.Infof("opening new tcp tunnel to %d", port.remote) + stream, err := p.resource.PortForward(p.name, v1alpha2.VirtualMachinePortForward{Port: port.remote, Protocol: port.protocol}) + if err != nil { + klog.Errorf("can't access vm/%s.%s: %v", p.name, p.namespace, err) + return + } + go p.handleConnection(conn, stream.AsConn(), port) + } +} + +// handleConnection copies data between the local connection and the stream to +// the remote server. +func (p *portForwarder) handleConnection(local, remote net.Conn, port forwardedPort) { + klog.Infof("handling tcp connection for %d", port.local) + errs := make(chan error) + go func() { + _, err := io.Copy(remote, local) + errs <- err + }() + go func() { + _, err := io.Copy(local, remote) + errs <- err + }() + + handleConnectionError(<-errs, port) + local.Close() + remote.Close() + handleConnectionError(<-errs, port) +} diff --git a/src/cli/internal/cmd/portforward/udp.go b/src/cli/internal/cmd/portforward/udp.go new file mode 100644 index 0000000000..9cdc8010f5 --- /dev/null +++ b/src/cli/internal/cmd/portforward/udp.go @@ -0,0 +1,150 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/portforward/udp.go +*/ + +package portforward + +import ( + "net" + "sync" + + "k8s.io/klog/v2" + + "github.com/deckhouse/virtualization/api/subresources/v1alpha2" +) + +const bufSize = 1500 + +func (p *portForwarder) startForwardingUDP(address *net.IPAddr, port forwardedPort) error { + listener, err := net.ListenUDP( + "udp", + &net.UDPAddr{ + IP: address.IP, + Zone: address.Zone, + Port: port.local, + }, + ) + if err != nil { + return err + } + + proxy := udpProxy{ + listener: listener, + remoteDialer: func() (net.Conn, error) { + klog.Infof("opening new udp tunnel to %d", port.remote) + stream, err := p.resource.PortForward(p.name, v1alpha2.VirtualMachinePortForward{Port: port.remote, Protocol: port.protocol}) + if err != nil { + klog.Errorf("can't access vm/%s.%s: %v", p.name, p.namespace, err) + return nil, err + } + return stream.AsConn(), nil + }, + clients: make(map[string]*udpProxyConn), + } + + go proxy.Run() + return nil +} + +type udpProxy struct { + listener *net.UDPConn + + remoteDialer func() (net.Conn, error) + + sync.Mutex + clients map[string]*udpProxyConn +} + +func (p *udpProxy) Run() { + buf := make([]byte, bufSize) + for { + if err := p.handleRead(buf); err != nil { + klog.Errorln(err) + } + } +} + +func (p *udpProxy) handleRead(buf []byte) error { + n, clientAddr, err := p.listener.ReadFromUDP(buf[0:]) + if err != nil { + return err + } + clientID := clientAddr.String() + + p.Lock() + defer p.Unlock() + + client, isKnownClient := p.clients[clientID] + + if !isKnownClient { + remoteConn, err := p.remoteDialer() + if err != nil { + return err + } + client = &udpProxyConn{ + localConn: p.listener, + clientAddr: clientAddr, + remoteConn: remoteConn, + close: make(chan struct{}), + } + p.clients[clientID] = client + go client.handleRemoteReads() + go p.cleanupClient(clientID, client) + } + + _, err = client.remoteConn.Write(buf[0:n]) + return err +} + +func (p *udpProxy) cleanupClient(clientID string, client *udpProxyConn) { + <-client.close + p.Lock() + defer p.Unlock() + delete(p.clients, clientID) +} + +type udpProxyConn struct { + localConn *net.UDPConn + clientAddr *net.UDPAddr + remoteConn net.Conn + + close chan struct{} +} + +func (c *udpProxyConn) handleRemoteReads() { + defer close(c.close) + buf := make([]byte, bufSize) + for { + if err := c.handleRemoteRead(buf); err != nil { + klog.Errorf("closing client: %v\n", err) + return + } + } +} + +func (c *udpProxyConn) handleRemoteRead(buf []byte) error { + n, err := c.remoteConn.Read(buf[0:]) + if err != nil { + return err + } + _, err = c.localConn.WriteToUDP(buf[0:n], c.clientAddr) + if err != nil { + return err + } + return nil +} diff --git a/src/cli/internal/cmd/scp/native.go b/src/cli/internal/cmd/scp/native.go new file mode 100644 index 0000000000..87cb46251e --- /dev/null +++ b/src/cli/internal/cmd/scp/native.go @@ -0,0 +1,128 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/scp/native.go +*/ + +package scp + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/povsister/scp" + + "github.com/deckhouse/virtualization/src/cli/internal/cmd/ssh" + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func (o *SCP) nativeSCP(local templates.LocalSCPArgument, remote templates.RemoteSCPArgument, toRemote bool) error { + sshClient := ssh.NativeSSHConnection{ + ClientConfig: o.clientConfig, + Options: o.options, + } + client, err := sshClient.PrepareSSHClient(remote.Namespace, remote.Name) + if err != nil { + return err + } + + scpClient, err := scp.NewClientFromExistingSSH(client, &scp.ClientOption{}) + if err != nil { + return err + } + + if toRemote { + return o.copyToRemote(scpClient, local.Path, remote.Path) + } + return o.copyFromRemote(scpClient, local.Path, remote.Path) +} + +func (o *SCP) copyToRemote(client *scp.Client, localPath, remotePath string) error { + isFile, isDir, exists, err := stat(localPath) + if err != nil { + return fmt.Errorf("failed reading path %q: %v", localPath, err) + } + + if !exists { + return fmt.Errorf("local path %q does not exist, can't copy it", localPath) + } + + if o.recursive { + if isFile { + return fmt.Errorf("local path %q is not a directory but '--recursive' was provided", localPath) + } + + return client.CopyDirToRemote(localPath, remotePath, &scp.DirTransferOption{PreserveProp: o.preserve}) + } + + if isDir { + return fmt.Errorf("local path %q is a directory but '--recursive' was not provided", localPath) + } + + return client.CopyFileToRemote(localPath, remotePath, &scp.FileTransferOption{PreserveProp: o.preserve}) +} + +func (o *SCP) copyFromRemote(client *scp.Client, localPath, remotePath string) error { + _, isDir, exists, err := stat(localPath) + if err != nil { + return fmt.Errorf("failed reading path %q: %v", localPath, err) + } + + if o.recursive { + if exists { + if !isDir { + return fmt.Errorf("local path %q is a file but '--recursive' was provided", localPath) + } + localPath = appendRemoteBase(localPath, remotePath) + } + + if err := os.MkdirAll(localPath, os.ModePerm); err != nil { + return fmt.Errorf("failed ensuring the existence of the local target directory %q: %v", localPath, err) + } + + return client.CopyDirFromRemote(remotePath, localPath, &scp.DirTransferOption{PreserveProp: o.preserve}) + } + + if exists && isDir { + localPath = appendRemoteBase(localPath, remotePath) + } + + return client.CopyFileFromRemote(remotePath, localPath, &scp.FileTransferOption{PreserveProp: o.preserve}) +} + +func stat(path string) (isFile, isDir, exists bool, err error) { + s, err := os.Stat(path) + if errors.Is(err, os.ErrNotExist) { + return false, false, false, nil + } else if err != nil { + return false, false, false, err + } + return !s.IsDir(), s.IsDir(), true, nil +} + +func appendRemoteBase(localPath, remotePath string) string { + remoteBase := filepath.Base(remotePath) + switch remoteBase { + case "..", ".", "/", "./", "": + // no identifiable base name, let's go with the supplied local path + return localPath + default: + // we identified a base location, let's append it to the local path + return filepath.Join(localPath, remoteBase) + } +} diff --git a/src/cli/internal/cmd/scp/scp.go b/src/cli/internal/cmd/scp/scp.go new file mode 100644 index 0000000000..e6a6cd1a43 --- /dev/null +++ b/src/cli/internal/cmd/scp/scp.go @@ -0,0 +1,118 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/scp/scp.go +*/ + +package scp + +import ( + "github.com/spf13/cobra" + + "k8s.io/client-go/tools/clientcmd" + + "github.com/deckhouse/virtualization/src/cli/internal/cmd/ssh" + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +const ( + recursiveFlag, recursiveFlagShort = "recursive", "r" + preserveFlag = "preserve" +) + +func NewCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + c := &SCP{ + clientConfig: clientConfig, + options: ssh.DefaultSSHOptions(), + } + c.options.LocalClientName = "scp" + + cmd := &cobra.Command{ + Use: "scp VirtualMachine)", + Short: "SCP files from/to a virtual machine.", + Example: usage(), + Args: templates.ExactArgs("scp", 2), + RunE: func(cmd *cobra.Command, args []string) error { + return c.Run(cmd, args) + }, + } + + ssh.AddCommandlineArgs(cmd.Flags(), &c.options) + cmd.Flags().BoolVarP(&c.recursive, recursiveFlag, recursiveFlagShort, c.recursive, + "Recursively copy entire directories") + cmd.Flags().BoolVar(&c.preserve, preserveFlag, c.preserve, + "Preserves modification times, access times, and modes from the original file.") + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} + +type SCP struct { + clientConfig clientcmd.ClientConfig + options ssh.SSHOptions + recursive bool + preserve bool +} + +func (o *SCP) Run(cmd *cobra.Command, args []string) error { + local, remote, toRemote, err := PrepareCommand(cmd, o.clientConfig, &o.options, args) + if err != nil { + return err + } + + if o.options.WrapLocalSSH { + clientArgs := o.buildSCPTarget(local, remote, toRemote) + return ssh.RunLocalClient(cmd, remote.Namespace, remote.Name, &o.options, clientArgs) + } + + return o.nativeSCP(local, remote, toRemote) +} + +func PrepareCommand(cmd *cobra.Command, clientConfig clientcmd.ClientConfig, opts *ssh.SSHOptions, args []string) (local templates.LocalSCPArgument, remote templates.RemoteSCPArgument, toRemote bool, err error) { + opts.IdentityFilePathProvided = cmd.Flags().Changed(ssh.IdentityFilePathFlag) + local, remote, toRemote, err = templates.ParseSCPArguments(args[0], args[1]) + if err != nil { + return + } + + if len(remote.Namespace) < 1 { + remote.Namespace, _, err = clientConfig.Namespace() + if err != nil { + return + } + } + + if len(remote.Username) > 0 { + opts.SSHUsername = remote.Username + } + return +} + +func usage() string { + return ` # Copy a file to the remote home folder of user "user" + {{ProgramName}} scp myfile.bin user@myvm:myfile.bin + + # Copy a directory to the remote home folder of user "user" + {{ProgramName}} scp --recursive ~/mydir/ user@myvm:./mydir + + # Copy a file to the remote home folder of user "user" without specifying a file name on the target + {{ProgramName}} scp myfile.bin user@myvm:. + + # Copy a file to 'myvm' in 'mynamespace' namespace + {{ProgramName}} scp myfile.bin user@myvm.mynamespace:myfile.bin + + # Copy a file from the remote location to a local folder + {{ProgramName}} scp user@myvm:myfile.bin ~/myfile.bin` +} diff --git a/src/cli/internal/cmd/scp/wrapped.go b/src/cli/internal/cmd/scp/wrapped.go new file mode 100644 index 0000000000..f5a7bb56c3 --- /dev/null +++ b/src/cli/internal/cmd/scp/wrapped.go @@ -0,0 +1,53 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/scp/wrapped.go +*/ + +package scp + +import ( + "strings" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func (o *SCP) buildSCPTarget(local templates.LocalSCPArgument, remote templates.RemoteSCPArgument, toRemote bool) (opts []string) { + if o.recursive { + opts = append(opts, "-r") + } + if o.preserve { + opts = append(opts, "-p") + } + + target := strings.Builder{} + if len(o.options.SSHUsername) > 0 { + target.WriteString(o.options.SSHUsername) + target.WriteRune('@') + } + target.WriteString(remote.Name) + target.WriteRune('.') + target.WriteString(remote.Namespace) + target.WriteRune(':') + target.WriteString(remote.Path) + + if toRemote { + opts = append(opts, local.Path, target.String()) + } else { + opts = append(opts, target.String(), local.Path) + } + return +} diff --git a/src/cli/internal/cmd/ssh/knownhosts.go b/src/cli/internal/cmd/ssh/knownhosts.go new file mode 100644 index 0000000000..5d8980f968 --- /dev/null +++ b/src/cli/internal/cmd/ssh/knownhosts.go @@ -0,0 +1,108 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/ssh/knownhosts.go +*/ + +package ssh + +import ( + "bufio" + "errors" + "fmt" + "net" + "os" + "strings" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" +) + +// InteractiveHostKeyCallback verifying the host key against known_hosts and adding the key if +// the user replies accordingly. +func InteractiveHostKeyCallback(knownHostsFilePath string) (ssh.HostKeyCallback, error) { + if _, err := os.Stat(knownHostsFilePath); errors.Is(err, os.ErrNotExist) { + f, err := os.Create(knownHostsFilePath) + if err != nil { + return nil, fmt.Errorf("failed creating known hosts file %q: %v", knownHostsFilePath, err) + } + _ = f.Close() + } else if err != nil { + return nil, fmt.Errorf("failed reading known host file %q: %v", knownHostsFilePath, err) + } + validator, err := knownhosts.New(knownHostsFilePath) + if err != nil { + return nil, err + } + + return func(hostname string, remote net.Addr, key ssh.PublicKey) error { + err := validator(hostname, remote, key) + if err == nil { + return nil + } + + var keyErr *knownhosts.KeyError + if errors.As(err, &keyErr) && len(keyErr.Want) == 0 { + shouldAdd, err := askToAddHostKey(hostname, remote, key) + if err != nil || !shouldAdd { + return err + } + if err := addHostKey(knownHostsFilePath, hostname, key); err != nil { + return err + } + return nil + } + + return err + }, nil +} + +func askToAddHostKey(hostname string, remote net.Addr, key ssh.PublicKey) (bool, error) { + reader := bufio.NewReader(os.Stdin) + fmt.Printf( + `The authenticity of host '%s (%s)' can't be established. +ECDSA key fingerprint is %s. +Are you sure you want to continue connecting (yes/no)? `, + hostname, remote, ssh.FingerprintSHA256(key), + ) + confirmation, err := reader.ReadString('\n') + if err != nil { + return false, err + } + confirmation = strings.TrimSpace(confirmation) + + if confirmation == "yes" { + return true, nil + } + if confirmation == "no" { + return false, nil + } + + fmt.Println("Please reply with either yes or no.") + return askToAddHostKey(hostname, remote, key) +} + +func addHostKey(knownHostsFilePath, hostname string, key ssh.PublicKey) error { + f, err := os.OpenFile(knownHostsFilePath, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer f.Close() + + addresses := []string{hostname} + _, err = fmt.Fprintln(f, knownhosts.Line(addresses, key)) + return err +} diff --git a/src/cli/internal/cmd/ssh/native.go b/src/cli/internal/cmd/ssh/native.go new file mode 100644 index 0000000000..524e000908 --- /dev/null +++ b/src/cli/internal/cmd/ssh/native.go @@ -0,0 +1,220 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/ssh/native.go +*/ + +package ssh + +import ( + "errors" + "fmt" + "net" + "os" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/api/subresources/v1alpha2" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/term" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" +) + +func (o *SSH) nativeSSH(namespace, name string) error { + conn := NativeSSHConnection{ + ClientConfig: o.clientConfig, + Options: o.options, + } + client, err := conn.PrepareSSHClient(namespace, name) + if err != nil { + return err + } + return conn.StartSession(client, o.command) +} + +type NativeSSHConnection struct { + ClientConfig clientcmd.ClientConfig + Options SSHOptions +} + +func (o *NativeSSHConnection) PrepareSSHClient(namespace, name string) (*ssh.Client, error) { + streamer, err := o.prepareSSHTunnel(namespace, name) + if err != nil { + return nil, err + } + + conn := streamer.AsConn() + addr := fmt.Sprintf("%s.%s:%d", name, namespace, o.Options.SSHPort) + authMethods := o.getAuthMethods(namespace, name) + + hostKeyCallback := ssh.InsecureIgnoreHostKey() + if len(o.Options.KnownHostsFilePath) > 0 { + hostKeyCallback, err = InteractiveHostKeyCallback(o.Options.KnownHostsFilePath) + if err != nil { + return nil, err + } + } else { + fmt.Println("WARNING: skipping hostkey check, provide --known-hosts to fix this") + } + + sshConn, chans, reqs, err := ssh.NewClientConn(conn, + addr, + &ssh.ClientConfig{ + HostKeyCallback: hostKeyCallback, + Auth: authMethods, + User: o.Options.SSHUsername, + }, + ) + if err != nil { + return nil, err + } + + return ssh.NewClient(sshConn, chans, reqs), nil +} + +func (o *NativeSSHConnection) getAuthMethods(namespace, name string) []ssh.AuthMethod { + var methods []ssh.AuthMethod + + methods = o.trySSHAgent(methods) + methods = o.tryPrivateKey(methods) + + methods = append(methods, ssh.PasswordCallback(func() (secret string, err error) { + password, err := readPassword(fmt.Sprintf("%s@%s.%s's password: ", o.Options.SSHUsername, name, namespace)) + fmt.Println() + return string(password), err + })) + + return methods +} + +func (o *NativeSSHConnection) trySSHAgent(methods []ssh.AuthMethod) []ssh.AuthMethod { + socket := os.Getenv("SSH_AUTH_SOCK") + if len(socket) < 1 { + return methods + } + conn, err := net.Dial("unix", socket) + if err != nil { + klog.Error("no connection to ssh agent, skipping agent authentication:", err) + return methods + } + agentClient := agent.NewClient(conn) + + return append(methods, ssh.PublicKeysCallback(agentClient.Signers)) +} + +func (o *NativeSSHConnection) tryPrivateKey(methods []ssh.AuthMethod) []ssh.AuthMethod { + // If the identity file at the default does not exist but was + // not explicitly provided, don't add the authentication mechanism. + if !o.Options.IdentityFilePathProvided { + if _, err := os.Stat(o.Options.IdentityFilePath); errors.Is(err, os.ErrNotExist) { + klog.V(3).Infof("No ssh key at the default location %q found, skipping RSA authentication.", o.Options.IdentityFilePath) + return methods + } + } + + callback := ssh.PublicKeysCallback(func() (signers []ssh.Signer, err error) { + key, err := os.ReadFile(o.Options.IdentityFilePath) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + var passphraseMissingError *ssh.PassphraseMissingError + if errors.As(err, &passphraseMissingError) { + signer, err = o.parsePrivateKeyWithPassphrase(key) + } + + if err != nil { + return nil, err + } + + return []ssh.Signer{signer}, nil + }) + + return append(methods, callback) +} + +func (o *NativeSSHConnection) parsePrivateKeyWithPassphrase(key []byte) (ssh.Signer, error) { + password, err := readPassword(fmt.Sprintf("Key %s requires a password: ", o.Options.IdentityFilePath)) + fmt.Println() + if err != nil { + return nil, err + } + + return ssh.ParsePrivateKeyWithPassphrase(key, password) +} + +func readPassword(reason string) ([]byte, error) { + fmt.Print(reason) + return term.ReadPassword(int(os.Stdin.Fd())) +} + +func (o *NativeSSHConnection) StartSession(client *ssh.Client, command string) error { + session, err := client.NewSession() + if err != nil { + return err + } + defer session.Close() + + session.Stdin = os.Stdin + session.Stderr = os.Stderr + session.Stdout = os.Stdout + + if command != "" { + if err := session.Run(command); err != nil { + return err + } + return nil + } + + restore, err := setupTerminal() + if err != nil { + return err + } + defer restore() + + if err := requestPty(session); err != nil { + return err + } + if err := session.Shell(); err != nil { + return err + } + + err = session.Wait() + var exitError *ssh.ExitError + if !errors.As(err, &exitError) { + return err + } + return nil +} + +func (o *NativeSSHConnection) prepareSSHTunnel(namespace, name string) (kubeclient.StreamInterface, error) { + virtCli, err := kubeclient.GetClientFromClientConfig(o.ClientConfig) + if err != nil { + return nil, err + } + opts := v1alpha2.VirtualMachinePortForward{ + Port: o.Options.SSHPort, + Protocol: "tcp", + } + stream, err := virtCli.VirtualMachines(namespace).PortForward(name, opts) + if err != nil { + return nil, fmt.Errorf("can't access VM %s: %w", name, err) + } + + return stream, nil +} diff --git a/src/cli/internal/cmd/ssh/ssh.go b/src/cli/internal/cmd/ssh/ssh.go new file mode 100644 index 0000000000..7de3d687f5 --- /dev/null +++ b/src/cli/internal/cmd/ssh/ssh.go @@ -0,0 +1,188 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/ssh/ssh.go +*/ + +package ssh + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" +) + +const ( + KnownHostsFileName = "d8virtualization_known_hosts" + portFlag, portFlagShort = "port", "p" + usernameFlag, usernameFlagShort = "username", "l" + IdentityFilePathFlag, identityFilePathFlagShort = "identity-file", "i" + knownHostsFilePathFlag = "known-hosts" + commandToExecute, commandToExecuteShort = "command", "c" + additionalOpts, additionalOptsShort = "local-ssh-opts", "t" + wrapLocalSSHFlag = "local-ssh" + wrapLocalSSHDefault = false +) + +type SSH struct { + clientConfig clientcmd.ClientConfig + options SSHOptions + command string +} + +type SSHOptions struct { + SSHPort int + SSHUsername string + IdentityFilePath string + IdentityFilePathProvided bool + KnownHostsFilePath string + KnownHostsFilePathDefault string + AdditionalSSHLocalOptions []string + WrapLocalSSH bool + LocalClientName string +} + +func DefaultSSHOptions() SSHOptions { + homeDir, err := os.UserHomeDir() + if err != nil { + klog.Warningf("failed to determine user home directory: %v", err) + } + options := SSHOptions{ + SSHPort: 22, + SSHUsername: defaultUsername(), + IdentityFilePath: filepath.Join(homeDir, ".ssh", "id_rsa"), + IdentityFilePathProvided: false, + KnownHostsFilePath: "", + KnownHostsFilePathDefault: "", + AdditionalSSHLocalOptions: []string{}, + WrapLocalSSH: wrapLocalSSHDefault, + LocalClientName: "ssh", + } + + if len(homeDir) > 0 { + options.KnownHostsFilePathDefault = filepath.Join(homeDir, ".ssh", KnownHostsFileName) + } + return options +} + +func defaultUsername() string { + vars := []string{ + "USER", // linux + "USERNAME", // linux, windows + "LOGNAME", // linux + } + for _, env := range vars { + if v := os.Getenv(env); v != "" { + return v + } + } + return "" +} + +func NewCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + c := &SSH{ + clientConfig: clientConfig, + options: DefaultSSHOptions(), + } + + cmd := &cobra.Command{ + Use: "ssh VirtualMachine", + Short: "Open a SSH connection to a virtual machine.", + Example: usage(), + Args: templates.ExactArgs("ssh", 1), + RunE: func(cmd *cobra.Command, args []string) error { + return c.Run(cmd, args) + }, + } + + AddCommandlineArgs(cmd.Flags(), &c.options) + cmd.Flags().StringVarP(&c.command, commandToExecute, commandToExecuteShort, c.command, + fmt.Sprintf(`--%s='ls /': Specify a command to execute in the VM`, commandToExecute)) + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} + +func AddCommandlineArgs(flagset *pflag.FlagSet, opts *SSHOptions) { + flagset.StringVarP(&opts.SSHUsername, usernameFlag, usernameFlagShort, opts.SSHUsername, + fmt.Sprintf("--%s=%s: Set this to the user you want to open the SSH connection as; If unassigned, this will be empty and the SSH default will apply", usernameFlag, opts.SSHUsername)) + flagset.StringVarP(&opts.IdentityFilePath, IdentityFilePathFlag, identityFilePathFlagShort, opts.IdentityFilePath, + fmt.Sprintf("--%s=/home/user/.ssh/id_rsa: Set the path to a private key used for authenticating to the server; If not provided, the client will try to use the local ssh-agent at $SSH_AUTH_SOCK", IdentityFilePathFlag)) + flagset.StringVar(&opts.KnownHostsFilePath, knownHostsFilePathFlag, opts.KnownHostsFilePathDefault, + fmt.Sprintf("--%s=/home/user/.ssh/%s: Set the path to the known_hosts file.", KnownHostsFileName, knownHostsFilePathFlag)) + flagset.IntVarP(&opts.SSHPort, portFlag, portFlagShort, opts.SSHPort, + fmt.Sprintf(`--%s=22: Specify a port on the VM to send SSH traffic to`, portFlag)) + + addAdditionalCommandlineArgs(flagset, opts) +} + +func (o *SSH) Run(cmd *cobra.Command, args []string) error { + namespace, name, err := PrepareCommand(cmd, o.clientConfig, &o.options, args) + if err != nil { + return err + } + + if o.options.WrapLocalSSH { + clientArgs := o.buildSSHTarget(namespace, name) + return RunLocalClient(cmd, namespace, name, &o.options, clientArgs) + } + + return o.nativeSSH(namespace, name) +} + +func PrepareCommand(cmd *cobra.Command, clientConfig clientcmd.ClientConfig, opts *SSHOptions, args []string) (namespace, name string, err error) { + opts.IdentityFilePathProvided = cmd.Flags().Changed(IdentityFilePathFlag) + var targetUsername string + namespace, name, targetUsername, err = templates.ParseSSHTarget(args[0]) + if err != nil { + return + } + + if len(namespace) < 1 { + namespace, _, err = clientConfig.Namespace() + if err != nil { + return + } + } + + if len(targetUsername) > 0 { + opts.SSHUsername = targetUsername + } + return +} + +func usage() string { + return fmt.Sprintf(` # Connect to 'myvm': + {{ProgramName}} ssh user@myvm [--%s] + + # Connect to 'myvm' in 'mynamespace' namespace + {{ProgramName}} ssh user@myvm.mynamespace [--%s] + + # Specify a username and namespace: + {{ProgramName}} ssh --namespace=mynamespace --%s=user myvm + + # Connect to 'myvm' using the local ssh binary found in $PATH: + {{ProgramName}} ssh --%s=true user@myvm`, + IdentityFilePathFlag, + IdentityFilePathFlag, + usernameFlag, + wrapLocalSSHFlag) +} diff --git a/src/cli/internal/cmd/ssh/terminal_unix.go b/src/cli/internal/cmd/ssh/terminal_unix.go new file mode 100644 index 0000000000..440a2c42de --- /dev/null +++ b/src/cli/internal/cmd/ssh/terminal_unix.go @@ -0,0 +1,88 @@ +//go:build !windows + +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/ssh/terminal_unix.go +*/ + +package ssh + +import ( + "encoding/binary" + "os" + "os/signal" + "syscall" + + "golang.org/x/crypto/ssh" + "golang.org/x/term" +) + +func setupTerminal() (func(), error) { + fd := int(os.Stdin.Fd()) + + state, err := term.MakeRaw(fd) + if err != nil { + return nil, err + } + + return func() { term.Restore(fd, state) }, nil +} + +func requestPty(session *ssh.Session) error { + w, h, err := term.GetSize(int(os.Stdin.Fd())) + if err != nil { + return err + } + + if err := session.RequestPty( + os.Getenv("TERM"), + h, w, + ssh.TerminalModes{}, + ); err != nil { + return err + } + + go resizeSessionOnWindowChange(session, os.Stdin.Fd()) + + return nil +} + +// resizeSessionOnWindowChange watches for SIGWINCH and refreshes the session with the new window size +func resizeSessionOnWindowChange(session *ssh.Session, _ uintptr) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGWINCH) + + for range sigs { + session.SendRequest("window-change", false, windowSizePayloadFor()) + } +} + +func windowSizePayloadFor() []byte { + w, h, err := term.GetSize(int(os.Stdin.Fd())) + if err != nil { + return buildWindowSizePayload(80, 24) + } + + return buildWindowSizePayload(w, h) +} + +func buildWindowSizePayload(width, height int) []byte { + size := make([]byte, 16) + binary.BigEndian.PutUint32(size, uint32(width)) + binary.BigEndian.PutUint32(size[4:], uint32(height)) + return size +} diff --git a/src/cli/internal/cmd/ssh/terminal_windows.go b/src/cli/internal/cmd/ssh/terminal_windows.go new file mode 100644 index 0000000000..603337934a --- /dev/null +++ b/src/cli/internal/cmd/ssh/terminal_windows.go @@ -0,0 +1,97 @@ +//go:build windows + +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/ssh/terminal_windows.go +*/ + +package ssh + +import ( + "os" + + "golang.org/x/crypto/ssh" + "golang.org/x/sys/windows" + "golang.org/x/term" +) + +func setupTerminal() (func(), error) { + fdIn := int(os.Stdin.Fd()) + fdOut := int(os.Stdout.Fd()) + handleIn := windows.Handle(fdIn) + handleOut := windows.Handle(fdOut) + + modeIn := uint32(0) + if err := windows.GetConsoleMode(handleIn, &modeIn); err != nil { + return nil, err + } + + modeOut := uint32(0) + if err := windows.GetConsoleMode(handleOut, &modeOut); err != nil { + return nil, err + } + + // Set the same modes as PowerShell/openssh-portable + // See https://github.com/PowerShell/openssh-portable/blob/latestw_all/contrib/win32/win32compat/console.c#L129 + // For Windows console modes see also https://docs.microsoft.com/en-us/windows/console/setconsolemode + // Disable unwanted modes + newModeIn := modeIn &^ (windows.ENABLE_LINE_INPUT | windows.ENABLE_ECHO_INPUT | + windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_MOUSE_INPUT) + // Enable wanted modes + newModeIn |= (windows.ENABLE_WINDOW_INPUT | windows.ENABLE_VIRTUAL_TERMINAL_INPUT) + newModeOut := modeOut | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | + windows.DISABLE_NEWLINE_AUTO_RETURN + + if err := windows.SetConsoleMode(handleIn, newModeIn); err != nil { + return nil, err + } + if err := windows.SetConsoleMode(handleOut, newModeOut); err != nil { + // Try to restore saved input modes + windows.SetConsoleMode(handleIn, modeIn) + return nil, err + } + + return func() { + // Restore to initially saved modes + windows.SetConsoleMode(handleIn, modeIn) + windows.SetConsoleMode(handleOut, modeOut) + }, nil +} + +func requestPty(session *ssh.Session) error { + w, h, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + return err + } + + // Do the same as PowerShell/openssh-portable + // See https://github.com/PowerShell/openssh-portable/blob/latestw_all/contrib/win32/win32compat/wmain_common.c#L58 + term := os.Getenv("TERM") + if term == "" { + term = "xterm-256color" + } + + if err := session.RequestPty( + term, + h, w, + ssh.TerminalModes{}, + ); err != nil { + return err + } + + return nil +} diff --git a/src/cli/internal/cmd/ssh/wrapped.go b/src/cli/internal/cmd/ssh/wrapped.go new file mode 100644 index 0000000000..703522e836 --- /dev/null +++ b/src/cli/internal/cmd/ssh/wrapped.go @@ -0,0 +1,108 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/ssh/wrapped.go +*/ + +package ssh + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/klog/v2" +) + +func addAdditionalCommandlineArgs(flagset *pflag.FlagSet, opts *SSHOptions) { + flagset.StringArrayVarP(&opts.AdditionalSSHLocalOptions, additionalOpts, additionalOptsShort, opts.AdditionalSSHLocalOptions, + fmt.Sprintf(`--%s="-o StrictHostKeyChecking=no" : Additional options to be passed to the local ssh. This is applied only if local-ssh=true`, additionalOpts)) + flagset.BoolVar(&opts.WrapLocalSSH, wrapLocalSSHFlag, opts.WrapLocalSSH, + fmt.Sprintf("--%s=true: Set this to true to use the SSH/SCP client available on your system by using this command as ProxyCommand; If set to false, this will establish a SSH/SCP connection with limited capabilities provided by this client", wrapLocalSSHFlag)) +} + +func RunLocalClient(cmd *cobra.Command, namespace, name string, options *SSHOptions, clientArgs []string) error { + args := []string{"-o"} + args = append(args, buildProxyCommandOption(cmd, namespace, name, options.SSHPort)) + + if len(options.AdditionalSSHLocalOptions) > 0 { + args = append(args, options.AdditionalSSHLocalOptions...) + } + if options.IdentityFilePathProvided { + args = append(args, "-i", options.IdentityFilePath) + } + + args = append(args, clientArgs...) + + command := exec.Command(options.LocalClientName, args...) + klog.V(3).Info("running: ", command) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Stdin = os.Stdin + + return command.Run() +} + +func buildProxyCommandOption(cmd *cobra.Command, namespace, name string, port int) string { + parents := make([]string, 0, 2) + for { + if !cmd.HasParent() { + break + } + cmd = cmd.Parent() + parents = append(parents, cmd.Name()) + } + parents[len(parents)-1] = os.Args[0] + pcmd := strings.Builder{} + + for i := 1; i <= len(parents); i++ { + pcmd.WriteString(parents[len(parents)-i]) + pcmd.WriteString(" ") + + } + + proxyCommand := strings.Builder{} + proxyCommand.WriteString("ProxyCommand=") + proxyCommand.WriteString(pcmd.String()) + proxyCommand.WriteString("port-forward --stdio=true ") + proxyCommand.WriteString(fmt.Sprintf("%s.%s", name, namespace)) + proxyCommand.WriteString(" ") + + proxyCommand.WriteString(strconv.Itoa(port)) + + return proxyCommand.String() +} + +func (o *SSH) buildSSHTarget(namespace, name string) (opts []string) { + target := strings.Builder{} + if len(o.options.SSHUsername) > 0 { + target.WriteString(o.options.SSHUsername) + target.WriteRune('@') + } + target.WriteString(name) + target.WriteRune('.') + target.WriteString(namespace) + + opts = append(opts, target.String()) + if o.command != "" { + opts = append(opts, o.command) + } + return +} diff --git a/src/cli/internal/cmd/vnc/vnc.go b/src/cli/internal/cmd/vnc/vnc.go new file mode 100644 index 0000000000..814d0d9332 --- /dev/null +++ b/src/cli/internal/cmd/vnc/vnc.go @@ -0,0 +1,392 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/vnc/vnc.go +*/ + +package vnc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/src/cli/internal/templates" + "github.com/gorilla/websocket" + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" +) + +const ( + ListenTimeout = 60 * time.Second + + // MACOSTigerVNCPATTERN + // #### Tiger VNC #### + // https://github.com/TigerVNC/tigervnc/releases + // Compatible with multiple Tiger VNC versions + MACOSTigerVNCPATTERN = `/Applications/TigerVNC Viewer*.app/Contents/MacOS/TigerVNC Viewer` + + // MACOSChickenVNC #### Chicken VNC #### + //# https://sourceforge.net/projects/chicken/ + MACOSChickenVNC = "/Applications/Chicken.app/Contents/MacOS/Chicken" + + // MACOSRealVNC + // #### Real VNC #### + // https://www.realvnc.com/en/connect/download/viewer/macos/ + MACOSRealVNC = "/Applications/VNC Viewer.app/Contents/MacOS/vncviewer" + + RemoteViewer = "remote-viewer" + TigerVNC = "vncviewer" +) + +var listenAddressFmt string + +var ( + listenAddress = "127.0.0.1" + proxyOnly bool + customPort = 0 +) + +func NewCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { + cmd := &cobra.Command{ + Use: "vnc VirtualMachine", + Short: "Open a vnc connection to a virtual machine.", + Example: usage(), + Args: templates.ExactArgs("vnc", 1), + RunE: func(cmd *cobra.Command, args []string) error { + c := VNC{clientConfig: clientConfig} + return c.Run(cmd, args) + }, + } + cmd.Flags().StringVar(&listenAddress, "address", listenAddress, "--address=127.0.0.1: Setting this will change the listening address of the VNC server. Example: --address=0.0.0.0 will make the server listen on all interfaces.") + cmd.Flags().BoolVar(&proxyOnly, "proxy-only", proxyOnly, "--proxy-only=false: Setting this true will run only the vnc proxy and show the port where VNC viewers can connect") + cmd.Flags().IntVar(&customPort, "port", customPort, + "--port=0: Assigning a port value to this will try to run the proxy on the given port if the port is accessible; If unassigned, the proxy will run on a random port") + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} + +type VNC struct { + clientConfig clientcmd.ClientConfig +} + +func (o *VNC) Run(cmd *cobra.Command, args []string) error { + namespace, vmName, err := templates.ParseTarget(args[0]) + if err != nil { + return err + } + if namespace == "" { + namespace, _, err = o.clientConfig.Namespace() + if err != nil { + return err + } + } + + virtCli, err := kubeclient.GetClientFromClientConfig(o.clientConfig) + if err != nil { + return err + } + + // Format the listening address to account for the port (ex: 127.0.0.0:5900) + // Set listenAddress to localhost if proxy-only flag is not set + if !proxyOnly { + listenAddress = "127.0.0.1" + klog.V(2).Infof("--proxy-only is set to false, listening on %s\n", listenAddress) + } + listenAddressFmt = listenAddress + ":%d" + lnAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(listenAddressFmt, customPort)) + if err != nil { + return fmt.Errorf("can't resolve the address: %s", err.Error()) + } + + // The local tcp server is used to proxy the podExec websock connection to vnc client + ln, err := net.ListenTCP("tcp", lnAddr) + if err != nil { + return fmt.Errorf("can't listen on unix socket: %s", err.Error()) + } + // End of pre-flight checks. Everything looks good, we can start + // the goroutines and let the data flow + + for { + err := connect(ln, virtCli, cmd, namespace, vmName) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return err + } + + var e *websocket.CloseError + if errors.As(err, &e) { + switch e.Code { + case websocket.CloseGoingAway: + fmt.Fprint(os.Stderr, "\nYou were disconnected from the console. This has one of the following reasons:"+ + "\n - another user connected to the console of the target vm\n") + return nil + case websocket.CloseAbnormalClosure: + fmt.Fprint(os.Stderr, "\nYou were disconnected from the console. This has one of the following reasons:"+ + "\n - network issues"+ + "\n - machine restart\n") + } + } else if strings.Contains(err.Error(), "interrupt") { + os.Exit(0) + } else { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + + time.Sleep(time.Second) + } + } +} + +func connect(ln *net.TCPListener, virtCli kubeclient.Client, cmd *cobra.Command, namespace, vmName string) (err error) { + vm, err := virtCli.VirtualMachines(namespace).Get(context.Background(), vmName, v1.GetOptions{}) + if err != nil { + return err + } + + if vm.Status.Phase != v1alpha2.MachineRunning { + return errors.New("VM is not running") + } + + // setup connection with VM + vnc, err := virtCli.VirtualMachines(namespace).VNC(vmName) + if err != nil { + return fmt.Errorf("can't access VM %s: %s", vmName, err.Error()) + } + + // -> pipeInWriter -> pipeInReader + // remote-viewer -> unix sock connection + // <- pipeOutReader <- pipeOutWriter + pipeInReader, pipeInWriter := io.Pipe() + pipeOutReader, pipeOutWriter := io.Pipe() + + k8ResChan := make(chan error) + listenResChan := make(chan error) + viewResChan := make(chan error) + stopChan := make(chan struct{}, 1) + doneChan := make(chan struct{}, 1) + writeStop := make(chan error) + readStop := make(chan error) + + go func() { + // transfer data from/to the VM + k8ResChan <- vnc.Stream(kubeclient.StreamOptions{ + In: pipeInReader, + Out: pipeOutWriter, + }) + }() + + // wait for vnc client to connect to our local proxy server + go func() { + start := time.Now() + klog.Infof("connection timeout: %v", ListenTimeout) + // Don't set deadline if only proxy is running and VNC is to be connected manually + if !proxyOnly { + // exit early if spawning vnc client fails + err := ln.SetDeadline(time.Now().Add(ListenTimeout)) + if err != nil { + listenResChan <- err + } + } + fd, err := ln.Accept() + if err != nil { + klog.V(2).Infof("Failed to accept unix sock connection. %s", err.Error()) + listenResChan <- err + } + defer fd.Close() + + klog.V(2).Infof("VNC Client connected in %v", time.Since(start)) + templates.PrintWarningForPausedVM(virtCli, vmName, namespace) + + // write to FD <- pipeOutReader + go func() { + _, err := io.Copy(fd, pipeOutReader) + readStop <- err + }() + + // read from FD -> pipeInWriter + go func() { + _, err := io.Copy(pipeInWriter, fd) + writeStop <- err + }() + + // don't terminate until vnc client is done + <-doneChan + listenResChan <- err + }() + + port := ln.Addr().(*net.TCPAddr).Port + + ctx, cancelCtx := context.WithCancel(context.Background()) + + if proxyOnly { + defer close(doneChan) + optionString, err := json.Marshal(struct { + Port int `json:"port"` + }{port}) + if err != nil { + return fmt.Errorf("error encountered: %s", err.Error()) + } + fmt.Fprintln(cmd.OutOrStdout(), string(optionString)) + } else { + // execute VNC Viewer + go checkAndRunVNCViewer(ctx, doneChan, viewResChan, port) + } + + go func() { + defer close(stopChan) + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + <-interrupt + viewResChan <- fmt.Errorf("interrupted") + }() + + select { + case <-stopChan: + case err = <-readStop: + case err = <-writeStop: + case err = <-k8ResChan: + case err = <-viewResChan: + case err = <-listenResChan: + } + + cancelCtx() + + return err +} + +func checkAndRunVNCViewer(ctx context.Context, doneChan chan struct{}, viewResChan chan error, port int) { + defer close(doneChan) + var err error + args := []string{} + + vncBin := "" + osType := runtime.GOOS + switch osType { + case "darwin": + if matches, err := filepath.Glob(MACOSTigerVNCPATTERN); err == nil && len(matches) > 0 { + // Always use the latest version + vncBin = matches[len(matches)-1] + args = tigerVncArgs(port) + } else if errors.Is(err, filepath.ErrBadPattern) { + viewResChan <- err + return + } else if _, err := os.Stat(MACOSChickenVNC); err == nil { + vncBin = MACOSChickenVNC + args = chickenVncArgs(port) + } else if !errors.Is(err, os.ErrNotExist) { + viewResChan <- err + return + } else if _, err := os.Stat(MACOSRealVNC); err == nil { + vncBin = MACOSRealVNC + args = realVncArgs(port) + } else if !errors.Is(err, os.ErrNotExist) { + viewResChan <- err + return + } else if _, err := exec.LookPath(RemoteViewer); err == nil { + // fall back to user supplied script/binary in path + vncBin = RemoteViewer + args = remoteViewerArgs(port) + } else if !errors.Is(err, os.ErrNotExist) { + viewResChan <- err + return + } + case "linux", "windows": + if _, err := exec.LookPath(RemoteViewer); err == nil { + vncBin = RemoteViewer + args = remoteViewerArgs(port) + } else if _, err := exec.LookPath(TigerVNC); err == nil { + vncBin = TigerVNC + args = tigerVncArgs(port) + } else { + viewResChan <- fmt.Errorf("could not find %s or %s binary in $PATH", + RemoteViewer, TigerVNC) + viewResChan <- err + return + } + default: + viewResChan <- fmt.Errorf("virtctl does not support VNC on %v", osType) + return + } + + if vncBin == "" { + klog.Errorf("No supported VNC app found in %s", osType) + err = fmt.Errorf("no supported VNC app found in %s", osType) + } else { + klog.V(4).Infof("Executing commandline: '%s %v'", vncBin, args) + // #nosec No risk for attacket injection. vncBin and args include predefined strings + cmd := exec.CommandContext(ctx, vncBin, args...) + output, err := cmd.CombinedOutput() + if err != nil { + klog.Errorf("%s execution failed: %v, output: %v", vncBin, err, string(output)) + } else { + klog.V(2).Infof("%v output: %v", vncBin, string(output)) + } + } + viewResChan <- err +} + +func tigerVncArgs(port int) (args []string) { + args = append(args, fmt.Sprintf(listenAddressFmt, port)) + if klog.V(4).Enabled() { + args = append(args, "Log=*:stderr:100") + } + return +} + +func chickenVncArgs(port int) (args []string) { + args = append(args, fmt.Sprintf(listenAddressFmt, port)) + return +} + +func realVncArgs(port int) (args []string) { + args = append(args, fmt.Sprintf(listenAddressFmt, port)) + args = append(args, "-WarnUnencrypted=0") + args = append(args, "-Shared=0") + args = append(args, "-ShareFiles=0") + if klog.V(4).Enabled() { + args = append(args, "-log=*:stderr:100") + } + return +} + +func remoteViewerArgs(port int) (args []string) { + args = append(args, fmt.Sprintf("vnc://127.0.0.1:%d", port)) + if klog.V(4).Enabled() { + args = append(args, "--debug") + } + return +} + +func usage() string { + return ` # Connect to 'testvm' via remote-viewer: + {{ProgramName}} vnc myvm + {{ProgramName}} vnc myvm.mynamespace + {{ProgramName}} vnc myvm -n mynamespace` +} diff --git a/src/cli/internal/templates/target.go b/src/cli/internal/templates/target.go new file mode 100644 index 0000000000..c6c1407782 --- /dev/null +++ b/src/cli/internal/templates/target.go @@ -0,0 +1,107 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/templates/target.go +*/ + +package templates + +import ( + "errors" + "fmt" + "strings" +) + +// ParseTarget argument supporting the form of name.namespace +func ParseTarget(arg string) (namespace, name string, err error) { + if len(arg) == 0 { + return "", "", errors.New("name is empty") + } + if arg[0] == '.' { + return "", "", errors.New("expected name before '.'") + } + if arg[len(arg)-1] == '.' { + return "", "", errors.New("expected namespace after '.'") + } + + parts := strings.FieldsFunc(arg, func(r rune) bool { + return r == '.' + }) + + name = parts[0] + + if len(parts) > 1 { + namespace = parts[1] + } + + return namespace, name, nil +} + +// ParseSSHTarget argument supporting the form of username@name.namespace +func ParseSSHTarget(arg string) (namespace, name, username string, err error) { + usernameAndTarget := strings.Split(arg, "@") + if len(usernameAndTarget) > 1 { + username = usernameAndTarget[0] + if len(username) < 1 { + return "", "", "", errors.New("expected username before '@'") + } + arg = usernameAndTarget[1] + } + + if len(arg) < 1 { + return "", "", "", errors.New("expected target after '@'") + } + + namespace, name, err = ParseTarget(arg) + return namespace, name, username, err +} + +type LocalSCPArgument struct { + Path string +} + +type RemoteSCPArgument struct { + Namespace string + Name string + Username string + Path string +} + +func ParseSCPArguments(arg1 string, arg2 string) (local LocalSCPArgument, remote RemoteSCPArgument, toRemote bool, err error) { + remoteArg := arg1 + localArg := arg2 + toRemote = false + if strings.Contains(arg1, ":") && strings.Contains(arg2, ":") { + err = fmt.Errorf("copying from a remote location to another remote location is not supported: %q to %q", arg1, arg2) + return + } else if !strings.Contains(arg1, ":") && !strings.Contains(arg2, ":") { + err = fmt.Errorf("none of the two provided locations seems to be a remote location: %q to %q", arg1, arg2) + return + } else if strings.Contains(localArg, ":") { + remoteArg = arg2 + localArg = arg1 + toRemote = true + } + + split := strings.SplitN(remoteArg, ":", 2) + remote.Namespace, remote.Name, remote.Username, err = ParseSSHTarget(split[0]) + if err != nil { + return + } + remote.Path = split[1] + local.Path = localArg + return +} diff --git a/src/cli/internal/templates/templates.go b/src/cli/internal/templates/templates.go new file mode 100644 index 0000000000..98f0e833f4 --- /dev/null +++ b/src/cli/internal/templates/templates.go @@ -0,0 +1,103 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/templates/templates.go +*/ + +package templates + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +// UsageTemplate returns the usage template for all subcommands +func UsageTemplate() string { + return `Usage:{{if .Runnable}} + {{prepare .UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{prepare .CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{prepare .Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{prepare .Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Use "{{ProgramName}} options" for a list of global command-line options (applies to all commands).{{end}} +` +} + +// MainUsageTemplate returns the usage template for the root command +func MainUsageTemplate() string { + return `Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{prepare .Short}}{{end}}{{end}} + +Use "{{ProgramName}} --help" for more information about a given command. +Use "{{ProgramName}} options" for a list of global command-line options (applies to all commands). +` +} + +// OptionsUsageTemplate returns a template which prints all global available commands +func OptionsUsageTemplate() string { + return `The following options can be passed to any command:{{if .HasAvailableInheritedFlags}} + +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}} +` +} + +// ExactArgs validate the number of input parameters +func ExactArgs(nameOfCommand string, n int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) == n { + return nil + } + err := errors.New("argument validation failed") + _, e := fmt.Fprintf(os.Stderr, "fatal: Number of input parameters is incorrect, %s accepts %d arg(s), received %d\n\n", nameOfCommand, n, len(args)) + if e != nil { + err = errors.Join(err, e) + } + e = cmd.Help() + if e != nil { + err = errors.Join(err, e) + } + return err + } +} + +// PrintWarningForPausedVM prints warning message if VM is paused +func PrintWarningForPausedVM(virtCli kubeclient.Client, vmName, namespace string) { + vm, err := virtCli.VirtualMachines(namespace).Get(context.Background(), vmName, metav1.GetOptions{}) + if err != nil { + return + } + if vm.Status.Phase == virtv2.MachinePause { + fmt.Fprintf(os.Stderr, "\rWarning: %s is paused. Console will be active after unpause.\n", vmName) + } +} diff --git a/src/cli/internal/util/console.go b/src/cli/internal/util/console.go new file mode 100644 index 0000000000..db74d9d407 --- /dev/null +++ b/src/cli/internal/util/console.go @@ -0,0 +1,103 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/utils/utils.go +*/ + +package util + +import ( + "errors" + "fmt" + "io" + "os" + + "golang.org/x/term" +) + +var ErrorInterrupt = errors.New("interrupt") + +func AttachConsole(stdinReader, stdoutReader *io.PipeReader, stdinWriter, stdoutWriter *io.PipeWriter, name string, resChan <-chan error) (err error) { + writeStop := make(chan error) + readStop := make(chan error) + if term.IsTerminal(int(os.Stdin.Fd())) { + state, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return fmt.Errorf("make raw terminal failed: %w", err) + } + defer term.Restore(int(os.Stdin.Fd()), state) + } + + fmt.Fprintf(os.Stderr, "Successfully connected to %s console. The escape sequence is ^]\n", name) + + out := os.Stdout + go func() { + defer close(readStop) + _, err := io.Copy(out, stdoutReader) + readStop <- err + }() + + stdinCh := make(chan []byte) + go func() { + in := os.Stdin + defer close(stdinCh) + buf := make([]byte, 1024) + for { + // reading from stdin + n, err := in.Read(buf) + if err != nil && err != io.EOF { + return + } + if n == 0 && err == io.EOF { + return + } + + // the escape sequence + if buf[0] == 29 { + return + } + + stdinCh <- buf[0:n] + } + }() + + go func() { + defer close(writeStop) + + stdinWriter.Write([]byte("\r")) + if err == io.EOF { + return + } + + for b := range stdinCh { + _, err = stdinWriter.Write(b) + if err == io.EOF { + return + } + } + + os.Exit(0) + }() + + select { + case err = <-writeStop: + return ErrorInterrupt + case err = <-readStop: + return ErrorInterrupt + case err = <-resChan: + return err + } +} diff --git a/src/cli/pkg/command/virtualization.go b/src/cli/pkg/command/virtualization.go new file mode 100644 index 0000000000..683bf36d3b --- /dev/null +++ b/src/cli/pkg/command/virtualization.go @@ -0,0 +1,97 @@ +/* +Copyright 2018 The KubeVirt Authors +Copyright 2024 Flant JSC + +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/main/pkg/virtctl/root.go +*/ + +package command + +import ( + "os" + "strings" + + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/component-base/logs" + + "github.com/deckhouse/virtualization/src/cli/internal/cmd/console" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/lifecycle" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/portforward" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/scp" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/ssh" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/vnc" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func NewCommand(programName string) (*cobra.Command, clientcmd.ClientConfig) { + // programName used in cobra templates to display either `d8 virtualization` or `d8vctl` + cobra.AddTemplateFunc( + "ProgramName", func() string { + return programName + }, + ) + + // used to enable replacement of `ProgramName` placeholder for cobra.Example, which has no template support + cobra.AddTemplateFunc( + "prepare", func(s string) string { + result := strings.Replace(s, "{{ProgramName}}", programName, -1) + return result + }, + ) + + virtCmd := &cobra.Command{ + Use: programName, + Short: programName + " controls virtual machine related operations on your kubernetes cluster.", + SilenceUsage: true, + SilenceErrors: true, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + logs.AddFlags(virtCmd.PersistentFlags()) + + virtCmd.SetUsageTemplate(templates.MainUsageTemplate()) + virtCmd.SetOut(os.Stdout) + + optionsCmd := &cobra.Command{ + Use: "options", + Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + cmd.Printf(cmd.UsageString()) + }, + } + + optionsCmd.SetUsageTemplate(templates.OptionsUsageTemplate()) + + clientConfig := kubeclient.DefaultClientConfig(virtCmd.PersistentFlags()) + virtCmd.AddCommand( + console.NewCommand(clientConfig), + vnc.NewCommand(clientConfig), + portforward.NewCommand(clientConfig), + ssh.NewCommand(clientConfig), + scp.NewCommand(clientConfig), + lifecycle.NewStartCommand(clientConfig), + lifecycle.NewStopCommand(clientConfig), + lifecycle.NewRestartCommand(clientConfig), + lifecycle.NewEvictCommand(clientConfig), + optionsCmd, + ) + return virtCmd, clientConfig +}