diff --git a/Makefile b/Makefile index 3108869..acff029 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,6 @@ clean: .PHONY: build build: mkdir -p ./bin - export CGO_ENABLED=0 - go build -o ./bin/lobbyd daemon/*.go + export CGO_ENABLED=0 && go build -o ./bin/lobbyd daemon/*.go + export CGO_ENABLED=0 && go build -o ./bin/lobbyctl ctl/*.go diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..8345672 --- /dev/null +++ b/client/main.go @@ -0,0 +1,176 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/go-resty/resty/v2" + "github.com/rosti-cz/server_lobby/server" +) + +// Encapsulation of Lobby's client code +type LobbyClient struct { + Proto string + Host string + Port uint + Token string +} + +func (l *LobbyClient) init() { + if len(l.Proto) == 0 { + l.Host = "http" + } + if len(l.Host) == 0 { + l.Host = "localhost" + } + if l.Port == 0 { + l.Port = 1313 + } +} + +// calls the backend API with given method, path and request body and returns status code, response body and error if there is any. +// Method can be GET, POST or DELETE. +// Path should start with / and it can contain query parameters too. +func (l *LobbyClient) call(method, path, body string) (uint, string, error) { + client := resty.New().R() + + if len(l.Token) != 0 { + client = client.SetHeader("Authorization", fmt.Sprintf("Token %s", l.Token)) + } + + if strings.ToUpper(method) == "GET" { + resp, err := client.Get(fmt.Sprintf("%s://%s:%d%s", l.Proto, l.Host, l.Port, path)) + if err != nil { + return 0, "", err + } + return uint(resp.StatusCode()), string(resp.Body()), nil + } else if strings.ToUpper(method) == "POST" { + resp, err := client.SetBody(body).Post(fmt.Sprintf("%s://%s:%d%s", l.Proto, l.Host, l.Port, path)) + if err != nil { + return 0, "", err + } + return uint(resp.StatusCode()), string(resp.Body()), nil + } else if strings.ToUpper(method) == "DELETE" { + resp, err := client.SetBody(body).Delete(fmt.Sprintf("%s://%s:%d%s", l.Proto, l.Host, l.Port, path)) + if err != nil { + return 0, "", err + } + return uint(resp.StatusCode()), string(resp.Body()), nil + } else { + return 0, "", errors.New("unsupported method") + } + +} + +// Returns discovery object of local machine +func (l *LobbyClient) GetDiscovery() (server.Discovery, error) { + l.init() + + var discovery server.Discovery + + path := "/v1/discovery" + method := "GET" + + status, body, err := l.call(method, path, "") + if err != nil { + return discovery, err + } + if status != 200 { + return discovery, fmt.Errorf("non-200 response: %s", body) + } + + err = json.Unmarshal([]byte(body), &discovery) + if err != nil { + return discovery, fmt.Errorf("response parsing error: %v", err) + } + + return discovery, nil +} + +// Returns all registered discovery packets +func (l *LobbyClient) GetDiscoveries() ([]server.Discovery, error) { + l.init() + + path := "/v1/discoveries" + method := "GET" + + var discoveries []server.Discovery + + status, body, err := l.call(method, path, "") + if err != nil { + return discoveries, err + } + if status != 200 { + return discoveries, fmt.Errorf("non-200 response: %s", body) + } + + err = json.Unmarshal([]byte(body), &discoveries) + if err != nil { + return discoveries, fmt.Errorf("response parsing error: %v", err) + } + + return discoveries, nil +} + +// Find discoveries by their labels +func (l *LobbyClient) FindByLabels(labels server.Labels) (server.Discoveries, error) { + l.init() + + path := fmt.Sprintf("/v1/discoveries?labels=%s", strings.Join(labels.StringSlice(), ",")) + method := "GET" + + var discoveries server.Discoveries + + status, body, err := l.call(method, path, "") + if err != nil { + return discoveries, err + } + if status != 200 { + return discoveries, fmt.Errorf("non-200 response: %s", body) + } + + err = json.Unmarshal([]byte(body), &discoveries) + if err != nil { + return discoveries, fmt.Errorf("response parsing error: %v", err) + } + + return discoveries, nil +} + +// Adds runtime labels for the local machine +func (l *LobbyClient) AddLabels(labels server.Labels) error { + l.init() + + path := "/v1/labels" + method := "POST" + + status, body, err := l.call(method, path, strings.Join(labels.StringSlice(), "\n")) + if err != nil { + return err + } + if status != 200 { + return fmt.Errorf("non-200 response: %s", body) + } + + return nil +} + +// Removes runtime labels of the local machine +func (l *LobbyClient) DeleteLabels(labels server.Labels) error { + l.init() + + path := "/v1/labels" + method := "DELETE" + + status, body, err := l.call(method, path, strings.Join(labels.StringSlice(), "\n")) + if err != nil { + return err + } + if status != 200 { + return fmt.Errorf("non-200 response: %s", body) + } + + return nil +} diff --git a/ctl/config.go b/ctl/config.go new file mode 100644 index 0000000..4a6dde1 --- /dev/null +++ b/ctl/config.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + + "github.com/kelseyhightower/envconfig" +) + +// Config keeps info about configuration of this daemon +type Config struct { + Token string `envconfig:"TOKEN" required:"false"` // Authentication token, if empty auth is disabled + Proto string `envconfig:"PROTOCOL" required:"false" default:"http"` // selected http or https protocols, default is http + Host string `envconfig:"HOST" required:"false" default:"127.0.0.1"` // IP address or hostname where lobbyd is listening + Port uint `envconfig:"PORT" required:"false" default:"1313"` // Same thing but the port part +} + +// GetConfig return configuration created based on environment variables +func GetConfig() *Config { + var config Config + + err := envconfig.Process("", &config) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + return &config +} diff --git a/ctl/format.go b/ctl/format.go new file mode 100644 index 0000000..97a674d --- /dev/null +++ b/ctl/format.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/rosti-cz/server_lobby/server" +) + +func printDiscovery(discovery server.Discovery) { + fmt.Printf("Hostname:\n %s\n", discovery.Hostname) + + if len(discovery.Labels) > 0 { + fmt.Printf("Labels:\n") + for _, label := range discovery.Labels { + fmt.Printf(" %s\n", label) + } + } +} + +func printDiscoveries(discoveries []server.Discovery) { + maxHostnameWidth := 0 + for _, discovery := range discoveries { + if len(discovery.Hostname) > maxHostnameWidth { + maxHostnameWidth = len(discovery.Hostname) + } + } + + for _, discovery := range discoveries { + if len(discovery.Labels) == 0 { + fmt.Println(discovery.Hostname) + } else { + hostname := fmt.Sprintf("%"+strconv.Itoa(maxHostnameWidth)+"s", discovery.Hostname) + fmt.Printf("%s %s\n", hostname, discovery.Labels[0].String()) + if len(discovery.Labels) > 1 { + for _, label := range discovery.Labels[1:] { + fmt.Printf("%"+strconv.Itoa(maxHostnameWidth+4)+"s%s\n", " ", label) + } + } + } + fmt.Println() + } +} diff --git a/ctl/main.go b/ctl/main.go new file mode 100644 index 0000000..8cb910c --- /dev/null +++ b/ctl/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/rosti-cz/server_lobby/client" + "github.com/rosti-cz/server_lobby/server" +) + +func Usage() { + flag.Usage() + fmt.Println("") + fmt.Println("Commands:") + fmt.Println(" discovery returns discovery packet of the server where the client is connected to") + fmt.Println(" discoveries returns list of all registered discovery packets") + fmt.Println(" labels add LABEL [LABEL] ... adds new runtime labels") + fmt.Println(" labels del LABEL [LABEL] ... deletes runtime labels") +} + +func main() { + config := GetConfig() + + // Setup flags + proto := flag.String("proto", "", "Select HTTP or HTTPS protocol") + host := flag.String("host", "", "Hostname or IP address of lobby daemon") + port := flag.Uint("port", 0, "Port of lobby daemon") + token := flag.String("token", "", "Token needed to communicate lobby daemon, if empty auth is disabled") + + flag.Parse() + + // Replace empty values from flags by values from environment variables + if *proto == "" { + proto = &config.Proto + } + if *host == "" { + host = &config.Host + } + if *port == 0 { + port = &config.Port + } + if *token == "" { + token = &config.Token + } + + // Validation + if *proto != "http" && *proto != "https" { + fmt.Println("Protocol can be only http or https") + } + + // Setup lobby client library + client := client.LobbyClient{ + Proto: strings.ToLower(*proto), + Host: *host, + Port: *port, + Token: *token, + } + + // Process rest of the arguments + if len(flag.Args()) == 0 { + Usage() + os.Exit(0) + } + + switch flag.Args()[0] { + case "discoveries": + discoveries, err := client.GetDiscoveries() + if err != nil { + fmt.Println(err) + } + printDiscoveries(discoveries) + case "discovery": + discovery, err := client.GetDiscovery() + if err != nil { + fmt.Println(err) + } + printDiscovery(discovery) + case "labels": + if len(flag.Args()) < 3 { + fmt.Println("ERROR: not enough arguments for labels command") + fmt.Println("") + Usage() + os.Exit(0) + } + + labels := server.Labels{} + labelsString := flag.Args()[2:] + for _, labelString := range labelsString { + labels = append(labels, server.Label(labelString)) + } + + if flag.Args()[1] == "add" { + err := client.AddLabels(labels) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(2) + } + } else if flag.Args()[1] == "del" { + err := client.DeleteLabels(labels) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(2) + } + } else { + fmt.Printf("ERROR: wrong labels subcommand\n\n") + Usage() + os.Exit(2) + } + + default: + Usage() + os.Exit(0) + } + +} diff --git a/go.mod b/go.mod index e5fcaab..07a9185 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/go-resty/resty/v2 v2.6.0 github.com/golang/protobuf v1.5.2 // indirect github.com/kelseyhightower/envconfig v1.4.0 github.com/labstack/echo v3.3.10+incompatible @@ -13,6 +14,5 @@ require ( github.com/shirou/gopsutil/v3 v3.21.7 github.com/stretchr/testify v1.7.0 github.com/valyala/fasttemplate v1.2.1 // indirect - golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index b949430..673a7a3 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4= +github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= 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= diff --git a/server/types.go b/server/types.go index 322c1ee..ebdf087 100644 --- a/server/types.go +++ b/server/types.go @@ -9,3 +9,14 @@ func (l Label) String() string { // Labels stores multiple Label records type Labels []Label + +// StringSlice return slice of Label as strings +func (l *Labels) StringSlice() []string { + labelsString := []string{} + + for _, label := range *l { + labelsString = append(labelsString, label.String()) + } + + return labelsString +}