diff --git a/cmd/vfkit/main.go b/cmd/vfkit/main.go index ac70994b..78726a20 100644 --- a/cmd/vfkit/main.go +++ b/cmd/vfkit/main.go @@ -1,3 +1,4 @@ +//go:build darwin // +build darwin /* @@ -28,6 +29,7 @@ import ( "github.com/Code-Hex/vz/v3" "github.com/crc-org/vfkit/pkg/cmdline" "github.com/crc-org/vfkit/pkg/config" + "github.com/crc-org/vfkit/pkg/rest" "github.com/crc-org/vfkit/pkg/vf" "github.com/docker/go-units" log "github.com/sirupsen/logrus" @@ -107,18 +109,45 @@ func waitForVMState(vm *vz.VirtualMachine, state vz.VirtualMachineState) error { } } -func runVirtualMachine(vmConfig *config.VirtualMachine) error { - vzVMConfig, err := vf.ToVzVirtualMachineConfig(vmConfig) +func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error { + uri, err := rest.ParseRestfulURI(opts.RestfulURI) if err != nil { return err } + scheme, err := rest.ToRestScheme(uri.Scheme) + if err != nil { + return err + } + + vzVMConfig, err := vf.ToVzVirtualMachineConfig(vmConfig) + if err != nil { + return err + } vm, err := vz.NewVirtualMachine(vzVMConfig) if err != nil { return err } + // Tuck a vm instance in the vmConfig for ability to call + // methods like state, etc. + vmConfig.VzVM = vm + + // Do not enable the rests server if user sets scheme to None + if scheme != rest.NONE { + go func() { + // start the restful service + srv := rest.NewServer(vmConfig, scheme, uri) + if err := srv.Start(); err != nil { + log.Error(err) + } + }() + } + return runVirtualMachine(vmConfig) +} - err = vm.Start() +func runVirtualMachine(vmConfig *config.VirtualMachine) error { + vm := vmConfig.VzVM + err := vm.Start() if err != nil { return err } diff --git a/cmd/vfkit/root.go b/cmd/vfkit/root.go index f5525d91..2bfa1bb0 100644 --- a/cmd/vfkit/root.go +++ b/cmd/vfkit/root.go @@ -5,6 +5,7 @@ import ( "os" "github.com/crc-org/vfkit/pkg/cmdline" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -18,11 +19,21 @@ var rootCmd = &cobra.Command{ Long: `A hypervisor written in Go using Apple's virtualization framework to run linux virtual machines. Complete documentation is available at https://github.com/crc-org/vfkit`, RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdline.ValidateInput(opts); err != nil { + return err + } + if len(opts.LogLevel) > 0 { + ll, err := getLogLevel() + if err != nil { + return err + } + logrus.SetLevel(ll) + } vmConfig, err := newVMConfiguration(opts) if err != nil { return err } - return runVirtualMachine(vmConfig) + return runVFKit(vmConfig, opts) }, Version: vfkitVersion, } @@ -43,6 +54,17 @@ func Execute() { } } +func getLogLevel() (logrus.Level, error) { + switch opts.LogLevel { + case "error": + return logrus.ErrorLevel, nil + case "debug": + return logrus.DebugLevel, nil + case "info": + return logrus.InfoLevel, nil + } + return 0, fmt.Errorf("unknown log level: %s", opts.LogLevel) +} func main() { Execute() } diff --git a/doc/usage.md b/doc/usage.md index b86ecfab..089a4a24 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -8,6 +8,18 @@ Specifying VM bootloader configuration is mandatory. Device configuration is optional, but most VM will need a disk image and a network interface to be configured. ## Generic Options + +- `--restful-URI` + +The URI (address) of the restful service. The default is `tcp://localhost:8081`. Valid schemes are +`tcp`, `none`, or `unix`. In the case of unix, the "host" portion would be a path to where the unix domain +socket will be stored. A scheme of `none` disables the restful service. + +- `--log-level` + +Set the log-level for VFKit. Values are the typical golang log levels such as `debug`, `info`, `error`, `warn`, +and `trace` among others. + ### Virtual Machine Resources These options specify the amount of RAM and the number of CPUs which will be available to the virtual machine. @@ -203,3 +215,31 @@ The share can be mounted in the guest with `mount -t virtio-fs vfkitTag /mnt`, w #### Example `--device virtio-fs,sharedDir=/Users/virtuser/vfkit/,mountTag=vfkit-share` + +## Restful Service + +### Get VM state + +Used to obtain the state of the virtual machine that is being run by VFKit. + +GET `/vm/state` +Response: {"state": "string"} + +### Change VM State + +Change the state of the virtual machine. Valid states are: +* Hardstop +* Pause +* Resume +* Stop + +POST `/vm/state` {"new_state": "new value"} + +Response: http 200 + +### Inspect VM + +Get description of the virtual machine + +GET `/vm/inspect` +Response: { "cpus": uint, "memory": uint64, "devices": []config.VirtIODevice } \ No newline at end of file diff --git a/go.mod b/go.mod index 5adf42d9..d14e8b53 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,23 @@ go 1.17 require ( github.com/Code-Hex/vz/v3 v3.0.4 github.com/docker/go-units v0.4.0 + github.com/gorilla/mux v1.8.0 github.com/h2non/filetype v1.1.3 github.com/prashantgupta24/mac-sleep-notifier v1.0.1 github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.6.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.1 + golang.org/x/sys v0.3.0 inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/mod v0.9.0 // indirect - golang.org/x/sys v0.3.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e35e5769..1d8d79fe 100644 --- a/go.sum +++ b/go.sum @@ -7,10 +7,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -23,8 +25,8 @@ github.com/prashantgupta24/mac-sleep-notifier v1.0.1/go.mod h1:bcfTio1xW+rjjZzdF github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/pkg/cmdline/cmdline.go b/pkg/cmdline/cmdline.go index 7e2eeb14..85ffd566 100644 --- a/pkg/cmdline/cmdline.go +++ b/pkg/cmdline/cmdline.go @@ -1,6 +1,9 @@ package cmdline -import "github.com/spf13/cobra" +import ( + "github.com/crc-org/vfkit/pkg/rest" + "github.com/spf13/cobra" +) type Options struct { Vcpus uint @@ -15,8 +18,14 @@ type Options struct { TimeSync string Devices []string + + RestfulURI string + + LogLevel string } +const DefaultRestfulURI = "tcp://localhost:8081" + func AddFlags(cmd *cobra.Command, opts *Options) { cmd.Flags().StringVarP(&opts.VmlinuzPath, "kernel", "k", "", "path to the virtual machine linux kernel") cmd.Flags().StringVarP(&opts.KernelCmdline, "kernel-cmdline", "C", "", "linux kernel command line") @@ -34,6 +43,26 @@ func AddFlags(cmd *cobra.Command, opts *Options) { cmd.Flags().UintVarP(&opts.MemoryMiB, "memory", "m", 512, "virtual machine RAM size in mibibytes") cmd.Flags().StringVarP(&opts.TimeSync, "timesync", "t", "", "sync guest time when host wakes up from sleep") - cmd.Flags().StringArrayVarP(&opts.Devices, "device", "d", []string{}, "devices") + + cmd.Flags().StringVar(&opts.RestfulURI, "restful-uri", DefaultRestfulURI, "URI address for RestFul services") + cmd.Flags().StringVar(&opts.LogLevel, "log-level", "", "set log level") + +} + +func ValidateInput(opts *Options) error { + if err := validateRestfulURI(opts.RestfulURI); err != nil { + return err + } + // more parsing of opts can be done here + return nil +} + +func validateRestfulURI(inputURI string) error { + if inputURI != DefaultRestfulURI { + if _, err := rest.ParseRestfulURI(inputURI); err != nil { + return err + } + } + return nil } diff --git a/pkg/cmdline/cmdline_test.go b/pkg/cmdline/cmdline_test.go new file mode 100644 index 00000000..ca8f3ef2 --- /dev/null +++ b/pkg/cmdline/cmdline_test.go @@ -0,0 +1,64 @@ +package cmdline + +import "testing" + +func Test_validateRestfulURI(t *testing.T) { + type args struct { + inputURI string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid tcp", + args: args{ + inputURI: "tcp://localhost:8080", + }, + wantErr: false, + }, + { + name: "invalid tcp - no host", + args: args{ + inputURI: "tcp://", + }, + wantErr: true, + }, + { + name: "invalid scheme", + args: args{ + inputURI: "http://localhost", + }, + wantErr: true, + }, + { + name: "valid uds", + args: args{ + inputURI: "unix:///my/socket/goes/here/vfkit.sock", + }, + wantErr: false, + }, + { + name: "invalid uds - no host", + args: args{ + inputURI: "unix://", + }, + wantErr: true, + }, + { + name: "none", + args: args{ + inputURI: "none://", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateRestfulURI(tt.args.inputURI); (err != nil) != tt.wantErr { + t.Errorf("validateRestfulURI() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 3c053e7f..23fea9f6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "github.com/Code-Hex/vz/v3" "os" "os/exec" "strconv" @@ -16,6 +17,7 @@ type VirtualMachine struct { Bootloader Bootloader Devices []VirtioDevice Timesync *TimeSync + VzVM *vz.VirtualMachine } // TimeSync enables synchronization of the host time to the linux guest after the host was suspended. @@ -31,6 +33,9 @@ type VMComponent interface { ToCmdLine() ([]string, error) } +// VFVMState is so we can extend vz.VirtualMachineState with handy methods +type VFVMState vz.VirtualMachineState + // NewVirtualMachine creates a new VirtualMachine instance. The virtual machine // will use vcpus virtual CPUs and it will be allocated memoryBytes bytes of // RAM. bootloader specifies which kernel/initrd/kernel args it will be using. @@ -201,3 +206,17 @@ func timesyncFromCmdLine(optsStr string) (*TimeSync, error) { return ×ync, nil } + +// String is a simple wrapper to get a string representation of the VMState +func (s VFVMState) String() string { + switch vz.VirtualMachineState(s) { + case vz.VirtualMachineStatePaused: + return "Paused" + case vz.VirtualMachineStateRunning: + return "Running" + case vz.VirtualMachineStateStopped: + return "Stopped" + } + // I debated on what to do here but for now, unknown? + return "Unknown" +} diff --git a/pkg/config/state_change.go b/pkg/config/state_change.go new file mode 100644 index 00000000..bdc3ef47 --- /dev/null +++ b/pkg/config/state_change.go @@ -0,0 +1,40 @@ +package config + +import ( + "errors" + + "github.com/crc-org/vfkit/pkg/rest" + "github.com/sirupsen/logrus" +) + +// ErrNotImplemented Temporary Error Message +var ErrNotImplemented = errors.New("function not implemented yet") + +// ChangeState execute a state change (i.e. running to stopped) +func (vm *VirtualMachine) ChangeState(newState rest.StateChange) error { + return ErrNotImplemented +} + +// GetState returns state of the VM +func (vm *VirtualMachine) GetState() VFVMState { + return VFVMState(vm.VzVM.State()) +} + +func (vm *VirtualMachine) Pause() error { + logrus.Debug("pausing virtual machine") + return vm.VzVM.Pause() +} + +func (vm *VirtualMachine) Resume() error { + logrus.Debug("resuming machine") + return vm.VzVM.Resume() +} + +func (vm *VirtualMachine) Stop() error { + logrus.Debug("stopping machine") + return vm.VzVM.Stop() +} +func (vm *VirtualMachine) ForceStop() error { + logrus.Debug("force stopping machine") + return ErrNotImplemented +} diff --git a/pkg/rest/config.go b/pkg/rest/config.go new file mode 100644 index 00000000..2edfca30 --- /dev/null +++ b/pkg/rest/config.go @@ -0,0 +1,20 @@ +package rest + +type RestScheme int + +const ( + TCP RestScheme = iota + UNIX + NONE +) + +// StateChange is a string strong typing of values for changing +// the state of a virtual machine +type StateChange string + +const ( + Resume StateChange = "Resume" + Pause StateChange = "Pause" + Stop StateChange = "Stop" + ForceStop StateChange = "Hardstop" +) diff --git a/pkg/rest/config_test.go b/pkg/rest/config_test.go new file mode 100644 index 00000000..fb52942d --- /dev/null +++ b/pkg/rest/config_test.go @@ -0,0 +1,130 @@ +package rest + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "net/url" + "testing" +) + +func TestToRestScheme(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want RestScheme + wantErr assert.ErrorAssertionFunc + }{ + { + name: "valid none", + args: args{ + s: "none", + }, + want: NONE, + wantErr: assert.NoError, + }, + { + name: "valid unix", + args: args{ + s: "unix", + }, + want: UNIX, + wantErr: assert.NoError, + }, + { + name: "valid tcp", + args: args{ + s: "tcp", + }, + want: TCP, + wantErr: assert.NoError, + }, + { + name: "invalid input", + args: args{ + s: "foobar", + }, + want: 2, + wantErr: assert.Error, + }, + { + name: "case doesnt matter", + args: args{ + s: "UnIx", + }, + want: UNIX, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ToRestScheme(tt.args.s) + if !tt.wantErr(t, err, fmt.Sprintf("ToRestScheme(%v)", tt.args.s)) { + return + } + assert.Equalf(t, tt.want, got, "ToRestScheme(%v)", tt.args.s) + }) + } +} + +func TestParseRestfulURI(t *testing.T) { + type args struct { + inputURI string + } + tests := []struct { + name string + args args + want *url.URL + wantErr assert.ErrorAssertionFunc + }{ + { + name: "valid tcp", + args: args{ + inputURI: "tcp://localhost:8080", + }, + want: &url.URL{ + Scheme: "tcp", + Host: "localhost:8080", + }, + wantErr: assert.NoError, + }, + { + name: "valid unix", + args: args{ + inputURI: "unix:///var/tmp/socket.sock", + }, + want: &url.URL{ + Scheme: "unix", + Path: "/var/tmp/socket.sock", + }, + wantErr: assert.NoError, + }, + { + name: "tcp - no host information", + args: args{ + inputURI: "tcp://", + }, + want: nil, + wantErr: assert.Error, + }, + { + name: "unix - no path", + args: args{ + inputURI: "unix:///", + }, + want: nil, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseRestfulURI(tt.args.inputURI) + if !tt.wantErr(t, err, fmt.Sprintf("ParseRestfulURI(%v)", tt.args.inputURI)) { + return + } + assert.Equalf(t, tt.want, got, "ParseRestfulURI(%v)", tt.args.inputURI) + }) + } +} diff --git a/pkg/rest/define/config.go b/pkg/rest/define/config.go new file mode 100644 index 00000000..9f9b868c --- /dev/null +++ b/pkg/rest/define/config.go @@ -0,0 +1,26 @@ +package define + +import ( + "github.com/crc-org/vfkit/pkg/config" + "github.com/crc-org/vfkit/pkg/rest" +) + +// InspectResponse is used when responding to a request for +// information about the virtual machine +type InspectResponse struct { + CPUs uint `json:"cpus"` + Memory uint64 `json:"memory"` + Devices []config.VirtioDevice `json:"devices"` +} + +// StateResponse is for responding to a request for virtual +// machine state +type StateResponse struct { + State string `json:"StateResponse"` +} + +// StateChangeRequest is used by the resetful service consumer +// to ask for a virtual machine state change +type StateChangeRequest struct { + NewState rest.StateChange `json:"new_state"` +} diff --git a/pkg/rest/rest.go b/pkg/rest/rest.go new file mode 100644 index 00000000..0214ca13 --- /dev/null +++ b/pkg/rest/rest.go @@ -0,0 +1,141 @@ +package rest + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/crc-org/vfkit/pkg/config" + "github.com/crc-org/vfkit/pkg/rest/define" + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +// VFKitService is used for the restful service; it describes +// the variables of the service like host/path but also has +// the router object +type VFKitService struct { + Host string + Path string + Port int + router *mux.Router + Scheme RestScheme +} + +// Start initiates the already configured restful service +func (v *VFKitService) Start() error { + logrus.Debugf("starting rest service on %s", v.Host) + return http.ListenAndServe(v.Host, v.router) +} + +// NewServer creates a new restful service +func NewServer(vm *config.VirtualMachine, scheme RestScheme, uri *url.URL) *VFKitService { + r := mux.NewRouter() + s := VFKitService{ + router: r, + Host: uri.Host, + Scheme: scheme, + } + // Handlers for the restful service. This is where endpoints are defined. + r.HandleFunc("/vm/state", getVMState(vm)).Methods(http.MethodGet) + r.HandleFunc("/vm/state", setVMState(vm)).Methods(http.MethodPost) + r.HandleFunc("/vm/inspect", inspect(vm)).Methods(http.MethodGet) + return &s +} + +// inspect returns information about the virtual machine like hw resources +// and devices +func inspect(vm *config.VirtualMachine) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ii := define.InspectResponse{ + CPUs: vm.Vcpus, + Memory: vm.MemoryBytes, + Devices: vm.Devices, + } + if err := json.NewEncoder(w).Encode(ii); err != nil { + logrus.Error(err) + } + } +} + +// getVMState retrieves the current vm state +func getVMState(vm *config.VirtualMachine) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logrus.Debugf("%s %s", r.Method, r.URL.Path) + current := config.VFVMState(vm.GetState()) + s := define.StateResponse{State: current.String()} + if err := json.NewEncoder(w).Encode(s); err != nil { + logrus.Error(err) + } + } +} + +// setVMState requests a state change on a virtual machine. At this time only +// the following states are valid: +// Pause - pause a running machine +// Resume - resume a paused machine +// Stop - stops a running machine +// Forcestop - forceably stops a running machine +func setVMState(vm *config.VirtualMachine) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + response error + s define.StateChangeRequest + ) + if err := json.NewDecoder(r.Body).Decode(&s); err != nil { + logrus.Error("bad json") + return + } + switch s.NewState { + case Pause: + response = vm.Pause() + case Resume: + response = vm.Resume() + case Stop: + response = vm.Stop() + case ForceStop: + response = vm.ForceStop() + default: + logrus.Errorf("invalid new StateResponse: %s", s.NewState) + } + if response != nil { + logrus.Errorf("failed action %s: %q", s.NewState, response) + // send a httpd numerical response for "bad" + } + } +} + +// ParseRestfulURI validates the input URI and returns an URL object +func ParseRestfulURI(inputURI string) (*url.URL, error) { + restURI, err := url.ParseRequestURI(inputURI) + if err != nil { + return nil, err + } + scheme, err := ToRestScheme(restURI.Scheme) + if err != nil { + return nil, err + } + if scheme == TCP && len(restURI.Host) < 1 { + return nil, errors.New("invalid uri host: none provided") + } + if scheme == UNIX && len(restURI.Path) < 1 { + return nil, errors.New("invalid uri path: none provided") + } + return restURI, err +} + +// ToRestScheme converts a string to a RestScheme +func ToRestScheme(s string) (RestScheme, error) { + switch strings.ToUpper(s) { + case "NONE": + return NONE, nil + case "UNIX": + return UNIX, nil + case "TCP": + return TCP, nil + } + return NONE, fmt.Errorf("invalid scheme %s", s) +} diff --git a/pkg/util/strings.go b/pkg/util/strings.go index ddcc02c9..a74fbfd3 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -9,3 +9,15 @@ func TrimQuotes(str string) string { return str } + +func StringInSlice(st string, sl []string) bool { + if sl == nil { + return false + } + for _, s := range sl { + if st == s { + return true + } + } + return false +} diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go new file mode 100644 index 00000000..343925b3 --- /dev/null +++ b/pkg/util/strings_test.go @@ -0,0 +1,94 @@ +package util + +import "testing" + +func TestStringInSlice(t *testing.T) { + type args struct { + st string + sl []string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "valid find", + args: args{ + st: "apple", + sl: []string{"apple", "banana", "orange"}, + }, + want: true, + }, + { + name: "invalid find", + args: args{ + st: "grape", + sl: []string{"apple", "banana", "orange"}, + }, + want: false, + }, + { + name: "slice is nil", + args: args{ + st: "grape", + sl: nil, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StringInSlice(tt.args.st, tt.args.sl); got != tt.want { + t.Errorf("StringInSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTrimQuotes(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "valid", + args: args{ + str: "\"foobar\"", + }, + want: "foobar", + }, + { + name: "only front quote", + args: args{ + str: "\"foobar", + }, + want: "\"foobar", + }, + { + name: "only end quote", + args: args{ + str: "foobar\"", + }, + want: "foobar\"", + }, + { + name: "no quote", + args: args{ + str: "foobar", + }, + want: "foobar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := TrimQuotes(tt.args.str); got != tt.want { + t.Errorf("TrimQuotes() = %v, want %v", got, tt.want) + } + }) + } +}