diff --git a/discovery/config/config.go b/discovery/config/config.go index 820de1f7..27d8c0cc 100644 --- a/discovery/config/config.go +++ b/discovery/config/config.go @@ -27,6 +27,7 @@ import ( "github.com/prometheus/prometheus/discovery/openstack" "github.com/prometheus/prometheus/discovery/targetgroup" "github.com/prometheus/prometheus/discovery/triton" + "github.com/prometheus/prometheus/discovery/uyuni" "github.com/prometheus/prometheus/discovery/zookeeper" ) @@ -58,6 +59,8 @@ type ServiceDiscoveryConfig struct { AzureSDConfigs []*azure.SDConfig `yaml:"azure_sd_configs,omitempty"` // List of Triton service discovery configurations. TritonSDConfigs []*triton.SDConfig `yaml:"triton_sd_configs,omitempty"` + // List of Uyuni service discovery configurations. + UyuniSDConfigs []*uyuni.SDConfig `yaml:"uyuni_sd_configs,omitempty"` } // Validate validates the ServiceDiscoveryConfig. diff --git a/discovery/manager.go b/discovery/manager.go index 1dbdecc8..ac621f3e 100644 --- a/discovery/manager.go +++ b/discovery/manager.go @@ -37,6 +37,7 @@ import ( "github.com/prometheus/prometheus/discovery/marathon" "github.com/prometheus/prometheus/discovery/openstack" "github.com/prometheus/prometheus/discovery/triton" + "github.com/prometheus/prometheus/discovery/uyuni" "github.com/prometheus/prometheus/discovery/zookeeper" ) @@ -406,6 +407,11 @@ func (m *Manager) registerProviders(cfg sd_config.ServiceDiscoveryConfig, setNam return triton.New(log.With(m.logger, "discovery", "triton"), c) }) } + for _, c := range cfg.UyuniSDConfigs { + add(c, func() (Discoverer, error) { + return uyuni.NewDiscovery(c, log.With(m.logger, "discovery", "uyuni")), nil + }) + } if len(cfg.StaticConfigs) > 0 { add(setName, func() (Discoverer, error) { return &StaticProvider{TargetGroups: cfg.StaticConfigs}, nil diff --git a/discovery/uyuni/uyuni.go b/discovery/uyuni/uyuni.go new file mode 100644 index 00000000..fcaaad0f --- /dev/null +++ b/discovery/uyuni/uyuni.go @@ -0,0 +1,340 @@ +// Copyright 2019 The Prometheus Authors +// 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 uyuni + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + "sync" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kolo/xmlrpc" + "github.com/pkg/errors" + "github.com/prometheus/common/model" + + "github.com/prometheus/prometheus/discovery/refresh" + "github.com/prometheus/prometheus/discovery/targetgroup" +) + +const ( + uyuniLabel = model.MetaLabelPrefix + "uyuni_" + uyuniLabelEntitlements = uyuniLabel + "entitlements" +) + +// DefaultSDConfig is the default Uyuni SD configuration. +var DefaultSDConfig = SDConfig{ + RefreshInterval: model.Duration(1 * time.Minute), +} + +// Regular expression to extract port from formula data +var monFormulaRegex = regexp.MustCompile(`--(?:telemetry\.address|web\.listen-address)=\":([0-9]*)\"`) + +// SDConfig is the configuration for Uyuni based service discovery. +type SDConfig struct { + Host string `yaml:"host"` + User string `yaml:"username"` + Pass string `yaml:"password"` + RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` +} + +// Uyuni API Response structures +type clientRef struct { + ID int `xmlrpc:"id"` + Name string `xmlrpc:"name"` +} + +type systemDetail struct { + ID int `xmlrpc:"id"` + Hostname string `xmlrpc:"hostname"` + Entitlements []string `xmlrpc:"addon_entitlements"` +} + +type groupDetail struct { + ID int `xmlrpc:"id"` + Subscribed int `xmlrpc:"subscribed"` + SystemGroupName string `xmlrpc:"system_group_name"` +} + +type networkInfo struct { + IP string `xmlrpc:"ip"` +} + +type exporterConfig struct { + Args string `xmlrpc:"args"` + Enabled bool `xmlrpc:"enabled"` +} + +// Discovery periodically performs Uyuni API requests. It implements the Discoverer interface. +type Discovery struct { + *refresh.Discovery + client *http.Client + interval time.Duration + sdConfig *SDConfig + logger log.Logger +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultSDConfig + type plain SDConfig + err := unmarshal((*plain)(c)) + + if err != nil { + return err + } + if c.Host == "" { + return errors.New("Uyuni SD configuration requires a Host") + } + if c.User == "" { + return errors.New("Uyuni SD configuration requires a Username") + } + if c.Pass == "" { + return errors.New("Uyuni SD configuration requires a Password") + } + if c.RefreshInterval <= 0 { + return errors.New("Uyuni SD configuration requires RefreshInterval to be a positive integer") + } + return nil +} + +// Attempt to login in Uyuni Server and get an auth token +func login(rpcclient *xmlrpc.Client, user string, pass string) (string, error) { + var result string + err := rpcclient.Call("auth.login", []interface{}{user, pass}, &result) + return result, err +} + +// Logout from Uyuni API +func logout(rpcclient *xmlrpc.Client, token string) error { + err := rpcclient.Call("auth.logout", token, nil) + return err +} + +// Get system list +func listSystems(rpcclient *xmlrpc.Client, token string) ([]clientRef, error) { + var result []clientRef + err := rpcclient.Call("system.listSystems", token, &result) + return result, err +} + +// Get system details +func getSystemDetails(rpcclient *xmlrpc.Client, token string, systemID int) (systemDetail, error) { + var result systemDetail + err := rpcclient.Call("system.getDetails", []interface{}{token, systemID}, &result) + return result, err +} + +// Get list of groups a system belongs to +func listSystemGroups(rpcclient *xmlrpc.Client, token string, systemID int) ([]groupDetail, error) { + var result []groupDetail + err := rpcclient.Call("system.listGroups", []interface{}{token, systemID}, &result) + return result, err +} + +// GetSystemNetworkInfo lists client FQDNs +func getSystemNetworkInfo(rpcclient *xmlrpc.Client, token string, systemID int) (networkInfo, error) { + var result networkInfo + err := rpcclient.Call("system.getNetwork", []interface{}{token, systemID}, &result) + return result, err +} + +// Get formula data for a given system +func getSystemFormulaData(rpcclient *xmlrpc.Client, token string, systemID int, formulaName string) (map[string]exporterConfig, error) { + var result map[string]exporterConfig + err := rpcclient.Call("formula.getSystemFormulaData", []interface{}{token, systemID, formulaName}, &result) + return result, err +} + +// Get formula data for a given group +func getGroupFormulaData(rpcclient *xmlrpc.Client, token string, groupID int, formulaName string) (map[string]exporterConfig, error) { + var result map[string]exporterConfig + err := rpcclient.Call("formula.getGroupFormulaData", []interface{}{token, groupID, formulaName}, &result) + return result, err +} + +// Get exporter port configuration from Formula +func extractPortFromFormulaData(args string) (string, error) { + tokens := monFormulaRegex.FindStringSubmatch(args) + if len(tokens) < 1 { + return "", errors.New("Unable to find port in args: " + args) + } + return tokens[1], nil +} + +// Take a current formula structure and override values if the new config is set +// Used for calculating final formula values when using groups +func getCombinedFormula(combined map[string]exporterConfig, new map[string]exporterConfig) map[string]exporterConfig { + for k, v := range new { + if v.Enabled { + combined[k] = v + } + } + return combined +} + +// NewDiscovery returns a new file discovery for the given paths. +func NewDiscovery(conf *SDConfig, logger log.Logger) *Discovery { + d := &Discovery{ + interval: time.Duration(conf.RefreshInterval), + sdConfig: conf, + logger: logger, + } + d.Discovery = refresh.NewDiscovery( + logger, + "uyuni", + time.Duration(conf.RefreshInterval), + d.refresh, + ) + return d +} + +func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { + + config := d.sdConfig + apiURL := config.Host + "/rpc/api" + + // Check if the URL is valid and create rpc client + _, err := url.ParseRequestURI(apiURL) + if err != nil { + return nil, errors.Wrap(err, "Uyuni Server URL is not valid") + } + rpc, _ := xmlrpc.NewClient(apiURL, nil) + tg := &targetgroup.Group{Source: config.Host} + + // Login into Uyuni API and get auth token + token, err := login(rpc, config.User, config.Pass) + if err != nil { + return nil, errors.Wrap(err, "Unable to login to Uyuni API") + } + // Get list of managed clients from Uyuni API + clientList, err := listSystems(rpc, token) + if err != nil { + return nil, errors.Wrap(err, "Unable to get list of systems") + } + + // Iterate list of clients + if len(clientList) == 0 { + fmt.Printf("\tFound 0 systems.\n") + } else { + startTime := time.Now() + var wg sync.WaitGroup + wg.Add(len(clientList)) + + for _, cl := range clientList { + + go func(client clientRef) { + defer wg.Done() + rpcclient, _ := xmlrpc.NewClient(apiURL, nil) + netInfo := networkInfo{} + formulas := map[string]exporterConfig{} + groups := []groupDetail{} + + // Get the system details + details, err := getSystemDetails(rpcclient, token, client.ID) + + if err != nil { + level.Error(d.logger).Log("msg", "Unable to get system details", "clientId", client.ID, "err", err) + return + } + jsonDetails, _ := json.Marshal(details) + level.Debug(d.logger).Log("msg", "System details", "details", jsonDetails) + + // Check if system is monitoring entitled + for _, v := range details.Entitlements { + if v == "monitoring_entitled" { // golang has no native method to check if an element is part of a slice + + // Get network details + netInfo, err = getSystemNetworkInfo(rpcclient, token, client.ID) + if err != nil { + level.Error(d.logger).Log("msg", "getSystemNetworkInfo failed", "clientId", client.ID, "err", err) + return + } + + // Get list of groups this system is assigned to + candidateGroups, err := listSystemGroups(rpcclient, token, client.ID) + if err != nil { + level.Error(d.logger).Log("msg", "listSystemGroups failed", "clientId", client.ID, "err", err) + return + } + groups := []string{} + for _, g := range candidateGroups { + // get list of group formulas + // TODO: Put the resulting data on a map so that we do not have to repeat the call below for every system + if g.Subscribed == 1 { + groupFormulas, err := getGroupFormulaData(rpcclient, token, g.ID, "prometheus-exporters") + if err != nil { + level.Error(d.logger).Log("msg", "getGroupFormulaData failed", "groupId", client.ID, "err", err) + return + } + formulas = getCombinedFormula(formulas, groupFormulas) + // replace spaces with dashes on all group names + groups = append(groups, strings.ToLower(strings.ReplaceAll(g.SystemGroupName, " ", "-"))) + } + } + + // Get system formula list + systemFormulas, err := getSystemFormulaData(rpcclient, token, client.ID, "prometheus-exporters") + if err != nil { + level.Error(d.logger).Log("msg", "getSystemFormulaData failed", "clientId", client.ID, "err", err) + return + } + formulas = getCombinedFormula(formulas, systemFormulas) + + // Iterate list of formulas and check for enabled exporters + for k, v := range formulas { + if v.Enabled { + port, err := extractPortFromFormulaData(v.Args) + if err != nil { + level.Error(d.logger).Log("msg", "Invalid exporter port", "clientId", client.ID, "err", err) + return + } + targets := model.LabelSet{} + addr := fmt.Sprintf("%s:%s", netInfo.IP, port) + targets[model.AddressLabel] = model.LabelValue(addr) + targets["exporter"] = model.LabelValue(k) + targets["hostname"] = model.LabelValue(details.Hostname) + targets["groups"] = model.LabelValue(strings.Join(groups, ",")) + for _, g := range groups { + gname := fmt.Sprintf("grp:%s", g) + targets[model.LabelName(gname)] = model.LabelValue("active") + } + tg.Targets = append(tg.Targets, targets) + } + } + } + } + // Log debug information + if netInfo.IP != "" { + level.Info(d.logger).Log("msg", "Found monitored system", "Host", details.Hostname, + "Entitlements", fmt.Sprintf("%+v", details.Entitlements), + "Network", fmt.Sprintf("%+v", netInfo), "Groups", + fmt.Sprintf("%+v", groups), "Formulas", fmt.Sprintf("%+v", formulas)) + } + rpcclient.Close() + }(cl) + } + wg.Wait() + level.Info(d.logger).Log("msg", "Total discovery time", "time", time.Since(startTime)) + } + logout(rpc, token) + rpc.Close() + return []*targetgroup.Group{tg}, nil +} diff --git a/vendor/github.com/kolo/xmlrpc/LICENSE b/vendor/github.com/kolo/xmlrpc/LICENSE new file mode 100644 index 00000000..8103dd13 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2012 Dmitry Maksimov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/kolo/xmlrpc/README.md b/vendor/github.com/kolo/xmlrpc/README.md new file mode 100644 index 00000000..8113cfcc --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/README.md @@ -0,0 +1,89 @@ +[![GoDoc](https://godoc.org/github.com/kolo/xmlrpc?status.svg)](https://godoc.org/github.com/kolo/xmlrpc) + +## Overview + +xmlrpc is an implementation of client side part of XMLRPC protocol in Go language. + +## Status + +This project is in minimal maintenance mode with no further development. Bug fixes +are accepted, but it might take some time until they will be merged. + +## Installation + +To install xmlrpc package run `go get github.com/kolo/xmlrpc`. To use +it in application add `"github.com/kolo/xmlrpc"` string to `import` +statement. + +## Usage + + client, _ := xmlrpc.NewClient("https://bugzilla.mozilla.org/xmlrpc.cgi", nil) + result := struct{ + Version string `xmlrpc:"version"` + }{} + client.Call("Bugzilla.version", nil, &result) + fmt.Printf("Version: %s\n", result.Version) // Version: 4.2.7+ + +Second argument of NewClient function is an object that implements +[http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper) +interface, it can be used to get more control over connection options. +By default it initialized by http.DefaultTransport object. + +### Arguments encoding + +xmlrpc package supports encoding of native Go data types to method +arguments. + +Data types encoding rules: + +* int, int8, int16, int32, int64 encoded to int; +* float32, float64 encoded to double; +* bool encoded to boolean; +* string encoded to string; +* time.Time encoded to datetime.iso8601; +* xmlrpc.Base64 encoded to base64; +* slice encoded to array; + +Structs decoded to struct by following rules: + +* all public field become struct members; +* field name become member name; +* if field has xmlrpc tag, its value become member name. + +Server method can accept few arguments, to handle this case there is +special approach to handle slice of empty interfaces (`[]interface{}`). +Each value of such slice encoded as separate argument. + +### Result decoding + +Result of remote function is decoded to native Go data type. + +Data types decoding rules: + +* int, i4 decoded to int, int8, int16, int32, int64; +* double decoded to float32, float64; +* boolean decoded to bool; +* string decoded to string; +* array decoded to slice; +* structs decoded following the rules described in previous section; +* datetime.iso8601 decoded as time.Time data type; +* base64 decoded to string. + +## Implementation details + +xmlrpc package contains clientCodec type, that implements [rpc.ClientCodec](http://golang.org/pkg/net/rpc/#ClientCodec) +interface of [net/rpc](http://golang.org/pkg/net/rpc) package. + +xmlrpc package works over HTTP protocol, but some internal functions +and data type were made public to make it easier to create another +implementation of xmlrpc that works over another protocol. To encode +request body there is EncodeMethodCall function. To decode server +response Response data type can be used. + +## Contribution + +See [project status](#status). + +## Authors + +Dmitry Maksimov (dmtmax@gmail.com) diff --git a/vendor/github.com/kolo/xmlrpc/client.go b/vendor/github.com/kolo/xmlrpc/client.go new file mode 100644 index 00000000..3aa86ce2 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/client.go @@ -0,0 +1,170 @@ +package xmlrpc + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/rpc" + "net/url" + "sync" +) + +type Client struct { + *rpc.Client +} + +// clientCodec is rpc.ClientCodec interface implementation. +type clientCodec struct { + // url presents url of xmlrpc service + url *url.URL + + // httpClient works with HTTP protocol + httpClient *http.Client + + // cookies stores cookies received on last request + cookies http.CookieJar + + // responses presents map of active requests. It is required to return request id, that + // rpc.Client can mark them as done. + responses map[uint64]*http.Response + mutex sync.Mutex + + response *Response + + // ready presents channel, that is used to link request and it`s response. + ready chan uint64 + + // close notifies codec is closed. + close chan uint64 +} + +func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) { + httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args) + + if codec.cookies != nil { + for _, cookie := range codec.cookies.Cookies(codec.url) { + httpRequest.AddCookie(cookie) + } + } + + if err != nil { + return err + } + + var httpResponse *http.Response + httpResponse, err = codec.httpClient.Do(httpRequest) + + if err != nil { + return err + } + + if codec.cookies != nil { + codec.cookies.SetCookies(codec.url, httpResponse.Cookies()) + } + + codec.mutex.Lock() + codec.responses[request.Seq] = httpResponse + codec.mutex.Unlock() + + codec.ready <- request.Seq + + return nil +} + +func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) { + var seq uint64 + + select { + case seq = <-codec.ready: + case <-codec.close: + return errors.New("codec is closed") + } + + codec.mutex.Lock() + httpResponse := codec.responses[seq] + codec.mutex.Unlock() + + if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { + return fmt.Errorf("request error: bad status code - %d", httpResponse.StatusCode) + } + + respData, err := ioutil.ReadAll(httpResponse.Body) + + if err != nil { + return err + } + + httpResponse.Body.Close() + + resp := NewResponse(respData) + + if resp.Failed() { + response.Error = fmt.Sprintf("%v", resp.Err()) + } + + codec.response = resp + + response.Seq = seq + + codec.mutex.Lock() + delete(codec.responses, seq) + codec.mutex.Unlock() + + return nil +} + +func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) { + if v == nil { + return nil + } + + if err = codec.response.Unmarshal(v); err != nil { + return err + } + + return nil +} + +func (codec *clientCodec) Close() error { + if transport, ok := codec.httpClient.Transport.(*http.Transport); ok { + transport.CloseIdleConnections() + } + + close(codec.close) + + return nil +} + +// NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service. +func NewClient(requrl string, transport http.RoundTripper) (*Client, error) { + if transport == nil { + transport = http.DefaultTransport + } + + httpClient := &http.Client{Transport: transport} + + jar, err := cookiejar.New(nil) + + if err != nil { + return nil, err + } + + u, err := url.Parse(requrl) + + if err != nil { + return nil, err + } + + codec := clientCodec{ + url: u, + httpClient: httpClient, + close: make(chan uint64), + ready: make(chan uint64), + responses: make(map[uint64]*http.Response), + cookies: jar, + } + + return &Client{rpc.NewClientWithCodec(&codec)}, nil +} diff --git a/vendor/github.com/kolo/xmlrpc/client_test.go b/vendor/github.com/kolo/xmlrpc/client_test.go new file mode 100644 index 00000000..b429d4f8 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/client_test.go @@ -0,0 +1,141 @@ +// +build integration + +package xmlrpc + +import ( + "context" + "runtime" + "sync" + "testing" + "time" +) + +func Test_CallWithoutArgs(t *testing.T) { + client := newClient(t) + defer client.Close() + + var result time.Time + if err := client.Call("service.time", nil, &result); err != nil { + t.Fatalf("service.time call error: %v", err) + } +} + +func Test_CallWithOneArg(t *testing.T) { + client := newClient(t) + defer client.Close() + + var result string + if err := client.Call("service.upcase", "xmlrpc", &result); err != nil { + t.Fatalf("service.upcase call error: %v", err) + } + + if result != "XMLRPC" { + t.Fatalf("Unexpected result of service.upcase: %s != %s", "XMLRPC", result) + } +} + +func Test_CallWithTwoArgs(t *testing.T) { + client := newClient(t) + defer client.Close() + + var sum int + if err := client.Call("service.sum", []interface{}{2, 3}, &sum); err != nil { + t.Fatalf("service.sum call error: %v", err) + } + + if sum != 5 { + t.Fatalf("Unexpected result of service.sum: %d != %d", 5, sum) + } +} + +func Test_TwoCalls(t *testing.T) { + client := newClient(t) + defer client.Close() + + var upcase string + if err := client.Call("service.upcase", "xmlrpc", &upcase); err != nil { + t.Fatalf("service.upcase call error: %v", err) + } + + var sum int + if err := client.Call("service.sum", []interface{}{2, 3}, &sum); err != nil { + t.Fatalf("service.sum call error: %v", err) + } + +} + +func Test_FailedCall(t *testing.T) { + client := newClient(t) + defer client.Close() + + var result int + if err := client.Call("service.error", nil, &result); err == nil { + t.Fatal("expected service.error returns error, but it didn't") + } +} + +func Test_ConcurrentCalls(t *testing.T) { + client := newClient(t) + + call := func() { + var result time.Time + client.Call("service.time", nil, &result) + } + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + call() + wg.Done() + }() + } + + wg.Wait() + client.Close() +} + +func Test_CloseMemoryLeak(t *testing.T) { + expected := runtime.NumGoroutine() + + for i := 0; i < 3; i++ { + client := newClient(t) + client.Call("service.time", nil, nil) + client.Close() + } + + var actual int + + // It takes some time to stop running goroutinges. This function checks number of + // running goroutines. It finishes execution if number is same as expected or timeout + // has been reached. + func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + for { + select { + case <-ctx.Done(): + return + default: + actual = runtime.NumGoroutine() + if actual == expected { + return + } + } + } + }() + + if actual != expected { + t.Errorf("expected number of running goroutines to be %d, but got %d", expected, actual) + } +} + +func newClient(t *testing.T) *Client { + client, err := NewClient("http://localhost:5001", nil) + if err != nil { + t.Fatalf("Can't create client: %v", err) + } + + return client +} diff --git a/vendor/github.com/kolo/xmlrpc/decoder.go b/vendor/github.com/kolo/xmlrpc/decoder.go new file mode 100644 index 00000000..d4dcb19a --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/decoder.go @@ -0,0 +1,473 @@ +package xmlrpc + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "reflect" + "strconv" + "strings" + "time" +) + +const ( + iso8601 = "20060102T15:04:05" + iso8601Z = "20060102T15:04:05Z07:00" + iso8601Hyphen = "2006-01-02T15:04:05" + iso8601HyphenZ = "2006-01-02T15:04:05Z07:00" +) + +var ( + // CharsetReader is a function to generate reader which converts a non UTF-8 + // charset into UTF-8. + CharsetReader func(string, io.Reader) (io.Reader, error) + + timeLayouts = []string{iso8601, iso8601Z, iso8601Hyphen, iso8601HyphenZ} + invalidXmlError = errors.New("invalid xml") +) + +type TypeMismatchError string + +func (e TypeMismatchError) Error() string { return string(e) } + +type decoder struct { + *xml.Decoder +} + +func unmarshal(data []byte, v interface{}) (err error) { + dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))} + + if CharsetReader != nil { + dec.CharsetReader = CharsetReader + } + + var tok xml.Token + for { + if tok, err = dec.Token(); err != nil { + return err + } + + if t, ok := tok.(xml.StartElement); ok { + if t.Name.Local == "value" { + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr { + return errors.New("non-pointer value passed to unmarshal") + } + if err = dec.decodeValue(val.Elem()); err != nil { + return err + } + + break + } + } + } + + // read until end of document + err = dec.Skip() + if err != nil && err != io.EOF { + return err + } + + return nil +} + +func (dec *decoder) decodeValue(val reflect.Value) error { + var tok xml.Token + var err error + + if val.Kind() == reflect.Ptr { + if val.IsNil() { + val.Set(reflect.New(val.Type().Elem())) + } + val = val.Elem() + } + + var typeName string + for { + if tok, err = dec.Token(); err != nil { + return err + } + + if t, ok := tok.(xml.EndElement); ok { + if t.Name.Local == "value" { + return nil + } else { + return invalidXmlError + } + } + + if t, ok := tok.(xml.StartElement); ok { + typeName = t.Name.Local + break + } + + // Treat value data without type identifier as string + if t, ok := tok.(xml.CharData); ok { + if value := strings.TrimSpace(string(t)); value != "" { + if err = checkType(val, reflect.String); err != nil { + return err + } + + val.SetString(value) + return nil + } + } + } + + switch typeName { + case "struct": + ismap := false + pmap := val + valType := val.Type() + + if err = checkType(val, reflect.Struct); err != nil { + if checkType(val, reflect.Map) == nil { + if valType.Key().Kind() != reflect.String { + return fmt.Errorf("only maps with string key type can be unmarshalled") + } + ismap = true + } else if checkType(val, reflect.Interface) == nil && val.IsNil() { + var dummy map[string]interface{} + valType = reflect.TypeOf(dummy) + pmap = reflect.New(valType).Elem() + val.Set(pmap) + ismap = true + } else { + return err + } + } + + var fields map[string]reflect.Value + + if !ismap { + fields = make(map[string]reflect.Value) + + for i := 0; i < valType.NumField(); i++ { + field := valType.Field(i) + fieldVal := val.FieldByName(field.Name) + + if fieldVal.CanSet() { + if fn := field.Tag.Get("xmlrpc"); fn != "" { + fields[fn] = fieldVal + } else { + fields[field.Name] = fieldVal + } + } + } + } else { + // Create initial empty map + pmap.Set(reflect.MakeMap(valType)) + } + + // Process struct members. + StructLoop: + for { + if tok, err = dec.Token(); err != nil { + return err + } + switch t := tok.(type) { + case xml.StartElement: + if t.Name.Local != "member" { + return invalidXmlError + } + + tagName, fieldName, err := dec.readTag() + if err != nil { + return err + } + if tagName != "name" { + return invalidXmlError + } + + var fv reflect.Value + ok := true + + if !ismap { + fv, ok = fields[string(fieldName)] + } else { + fv = reflect.New(valType.Elem()) + } + + if ok { + for { + if tok, err = dec.Token(); err != nil { + return err + } + if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" { + if err = dec.decodeValue(fv); err != nil { + return err + } + + // + if err = dec.Skip(); err != nil { + return err + } + + break + } + } + } + + // + if err = dec.Skip(); err != nil { + return err + } + + if ismap { + pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv)) + val.Set(pmap) + } + case xml.EndElement: + break StructLoop + } + } + case "array": + slice := val + if checkType(val, reflect.Interface) == nil && val.IsNil() { + slice = reflect.ValueOf([]interface{}{}) + } else if err = checkType(val, reflect.Slice); err != nil { + return err + } + + ArrayLoop: + for { + if tok, err = dec.Token(); err != nil { + return err + } + + switch t := tok.(type) { + case xml.StartElement: + var index int + if t.Name.Local != "data" { + return invalidXmlError + } + DataLoop: + for { + if tok, err = dec.Token(); err != nil { + return err + } + + switch tt := tok.(type) { + case xml.StartElement: + if tt.Name.Local != "value" { + return invalidXmlError + } + + if index < slice.Len() { + v := slice.Index(index) + if v.Kind() == reflect.Interface { + v = v.Elem() + } + if v.Kind() != reflect.Ptr { + return errors.New("error: cannot write to non-pointer array element") + } + if err = dec.decodeValue(v); err != nil { + return err + } + } else { + v := reflect.New(slice.Type().Elem()) + if err = dec.decodeValue(v); err != nil { + return err + } + slice = reflect.Append(slice, v.Elem()) + } + + // + if err = dec.Skip(); err != nil { + return err + } + index++ + case xml.EndElement: + val.Set(slice) + break DataLoop + } + } + case xml.EndElement: + break ArrayLoop + } + } + default: + if tok, err = dec.Token(); err != nil { + return err + } + + var data []byte + + switch t := tok.(type) { + case xml.EndElement: + return nil + case xml.CharData: + data = []byte(t.Copy()) + default: + return invalidXmlError + } + + switch typeName { + case "int", "i4", "i8": + if checkType(val, reflect.Interface) == nil && val.IsNil() { + i, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return err + } + + pi := reflect.New(reflect.TypeOf(i)).Elem() + pi.SetInt(i) + val.Set(pi) + } else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil { + return err + } else { + i, err := strconv.ParseInt(string(data), 10, val.Type().Bits()) + if err != nil { + return err + } + + val.SetInt(i) + } + case "string", "base64": + str := string(data) + if checkType(val, reflect.Interface) == nil && val.IsNil() { + pstr := reflect.New(reflect.TypeOf(str)).Elem() + pstr.SetString(str) + val.Set(pstr) + } else if err = checkType(val, reflect.String); err != nil { + return err + } else { + val.SetString(str) + } + case "dateTime.iso8601": + var t time.Time + var err error + + for _, layout := range timeLayouts { + t, err = time.Parse(layout, string(data)) + if err == nil { + break + } + } + if err != nil { + return err + } + + if checkType(val, reflect.Interface) == nil && val.IsNil() { + ptime := reflect.New(reflect.TypeOf(t)).Elem() + ptime.Set(reflect.ValueOf(t)) + val.Set(ptime) + } else if _, ok := val.Interface().(time.Time); !ok { + return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind())) + } else { + val.Set(reflect.ValueOf(t)) + } + case "boolean": + v, err := strconv.ParseBool(string(data)) + if err != nil { + return err + } + + if checkType(val, reflect.Interface) == nil && val.IsNil() { + pv := reflect.New(reflect.TypeOf(v)).Elem() + pv.SetBool(v) + val.Set(pv) + } else if err = checkType(val, reflect.Bool); err != nil { + return err + } else { + val.SetBool(v) + } + case "double": + if checkType(val, reflect.Interface) == nil && val.IsNil() { + i, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return err + } + + pdouble := reflect.New(reflect.TypeOf(i)).Elem() + pdouble.SetFloat(i) + val.Set(pdouble) + } else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil { + return err + } else { + i, err := strconv.ParseFloat(string(data), val.Type().Bits()) + if err != nil { + return err + } + + val.SetFloat(i) + } + default: + return errors.New("unsupported type") + } + + // + if err = dec.Skip(); err != nil { + return err + } + } + + return nil +} + +func (dec *decoder) readTag() (string, []byte, error) { + var tok xml.Token + var err error + + var name string + for { + if tok, err = dec.Token(); err != nil { + return "", nil, err + } + + if t, ok := tok.(xml.StartElement); ok { + name = t.Name.Local + break + } + } + + value, err := dec.readCharData() + if err != nil { + return "", nil, err + } + + return name, value, dec.Skip() +} + +func (dec *decoder) readCharData() ([]byte, error) { + var tok xml.Token + var err error + + if tok, err = dec.Token(); err != nil { + return nil, err + } + + if t, ok := tok.(xml.CharData); ok { + return []byte(t.Copy()), nil + } else { + return nil, invalidXmlError + } +} + +func checkType(val reflect.Value, kinds ...reflect.Kind) error { + if len(kinds) == 0 { + return nil + } + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + match := false + + for _, kind := range kinds { + if val.Kind() == kind { + match = true + break + } + } + + if !match { + return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v", + val.Kind(), kinds[0])) + } + + return nil +} diff --git a/vendor/github.com/kolo/xmlrpc/decoder_test.go b/vendor/github.com/kolo/xmlrpc/decoder_test.go new file mode 100644 index 00000000..3701d50a --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/decoder_test.go @@ -0,0 +1,234 @@ +package xmlrpc + +import ( + "fmt" + "io" + "io/ioutil" + "reflect" + "testing" + "time" + + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/transform" +) + +type book struct { + Title string + Amount int +} + +type bookUnexported struct { + title string + amount int +} + +var unmarshalTests = []struct { + value interface{} + ptr interface{} + xml string +}{ + // int, i4, i8 + {0, new(*int), ""}, + {100, new(*int), "100"}, + {389451, new(*int), "389451"}, + {int64(45659074), new(*int64), "45659074"}, + + // string + {"Once upon a time", new(*string), "Once upon a time"}, + {"Mike & Mick ", new(*string), "Mike & Mick <London, UK>"}, + {"Once upon a time", new(*string), "Once upon a time"}, + + // base64 + {"T25jZSB1cG9uIGEgdGltZQ==", new(*string), "T25jZSB1cG9uIGEgdGltZQ=="}, + + // boolean + {true, new(*bool), "1"}, + {false, new(*bool), "0"}, + + // double + {12.134, new(*float32), "12.134"}, + {-12.134, new(*float32), "-12.134"}, + + // datetime.iso8601 + {_time("2013-12-09T21:00:12Z"), new(*time.Time), "20131209T21:00:12"}, + {_time("2013-12-09T21:00:12Z"), new(*time.Time), "20131209T21:00:12Z"}, + {_time("2013-12-09T21:00:12-01:00"), new(*time.Time), "20131209T21:00:12-01:00"}, + {_time("2013-12-09T21:00:12+01:00"), new(*time.Time), "20131209T21:00:12+01:00"}, + {_time("2013-12-09T21:00:12Z"), new(*time.Time), "2013-12-09T21:00:12"}, + {_time("2013-12-09T21:00:12Z"), new(*time.Time), "2013-12-09T21:00:12Z"}, + {_time("2013-12-09T21:00:12-01:00"), new(*time.Time), "2013-12-09T21:00:12-01:00"}, + {_time("2013-12-09T21:00:12+01:00"), new(*time.Time), "2013-12-09T21:00:12+01:00"}, + + // array + {[]int{1, 5, 7}, new(*[]int), "157"}, + {[]interface{}{"A", "5"}, new(interface{}), "A5"}, + {[]interface{}{"A", int64(5)}, new(interface{}), "A5"}, + + // struct + {book{"War and Piece", 20}, new(*book), "TitleWar and PieceAmount20"}, + {bookUnexported{}, new(*bookUnexported), "titleWar and Pieceamount20"}, + {map[string]interface{}{"Name": "John Smith"}, new(interface{}), "NameJohn Smith"}, + {map[string]interface{}{}, new(interface{}), ""}, +} + +func _time(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(fmt.Sprintf("time parsing error: %v", err)) + } + return t +} + +func Test_unmarshal(t *testing.T) { + for _, tt := range unmarshalTests { + v := reflect.New(reflect.TypeOf(tt.value)) + if err := unmarshal([]byte(tt.xml), v.Interface()); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + + v = v.Elem() + + if v.Kind() == reflect.Slice { + vv := reflect.ValueOf(tt.value) + if vv.Len() != v.Len() { + t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface()) + } + for i := 0; i < v.Len(); i++ { + if v.Index(i).Interface() != vv.Index(i).Interface() { + t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface()) + } + } + } else { + a1 := v.Interface() + a2 := interface{}(tt.value) + + if !reflect.DeepEqual(a1, a2) { + t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface()) + } + } + } +} + +func Test_unmarshalToNil(t *testing.T) { + for _, tt := range unmarshalTests { + if err := unmarshal([]byte(tt.xml), tt.ptr); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + } +} + +func Test_typeMismatchError(t *testing.T) { + var s string + + encoded := "100" + var err error + + if err = unmarshal([]byte(encoded), &s); err == nil { + t.Fatal("unmarshal error: expected error, but didn't get it") + } + + if _, ok := err.(TypeMismatchError); !ok { + t.Fatal("unmarshal error: expected type mistmatch error, but didn't get it") + } +} + +func Test_unmarshalEmptyValueTag(t *testing.T) { + var v int + + if err := unmarshal([]byte(""), &v); err != nil { + t.Fatalf("unmarshal error: %v", err) + } +} + +const structEmptyXML = ` + + + + +` + +func Test_unmarshalEmptyStruct(t *testing.T) { + var v interface{} + if err := unmarshal([]byte(structEmptyXML), &v); err != nil { + t.Fatal(err) + } + if v == nil { + t.Fatalf("got nil map") + } +} + +const arrayValueXML = ` + + + + 234 + 1 + Hello World + Extra Value + + + +` + +func Test_unmarshalExistingArray(t *testing.T) { + + var ( + v1 int + v2 bool + v3 string + + v = []interface{}{&v1, &v2, &v3} + ) + if err := unmarshal([]byte(arrayValueXML), &v); err != nil { + t.Fatal(err) + } + + // check pre-existing values + if want := 234; v1 != want { + t.Fatalf("want %d, got %d", want, v1) + } + if want := true; v2 != want { + t.Fatalf("want %t, got %t", want, v2) + } + if want := "Hello World"; v3 != want { + t.Fatalf("want %s, got %s", want, v3) + } + // check the appended result + if n := len(v); n != 4 { + t.Fatalf("missing appended result") + } + if got, ok := v[3].(string); !ok || got != "Extra Value" { + t.Fatalf("got %s, want %s", got, "Extra Value") + } +} + +func Test_decodeNonUTF8Response(t *testing.T) { + data, err := ioutil.ReadFile("fixtures/cp1251.xml") + if err != nil { + t.Fatal(err) + } + + CharsetReader = decode + + var s string + if err = unmarshal(data, &s); err != nil { + fmt.Println(err) + t.Fatal("unmarshal error: cannot decode non utf-8 response") + } + + expected := "Л.Н. Толстой - Война и Мир" + + if s != expected { + t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", expected, s) + } + + CharsetReader = nil +} + +func decode(charset string, input io.Reader) (io.Reader, error) { + if charset != "cp1251" { + return nil, fmt.Errorf("unsupported charset") + } + + return transform.NewReader(input, charmap.Windows1251.NewDecoder()), nil +} diff --git a/vendor/github.com/kolo/xmlrpc/encoder.go b/vendor/github.com/kolo/xmlrpc/encoder.go new file mode 100644 index 00000000..d585a7d3 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/encoder.go @@ -0,0 +1,171 @@ +package xmlrpc + +import ( + "bytes" + "encoding/xml" + "fmt" + "reflect" + "sort" + "strconv" + "time" +) + +type encodeFunc func(reflect.Value) ([]byte, error) + +func marshal(v interface{}) ([]byte, error) { + if v == nil { + return []byte{}, nil + } + + val := reflect.ValueOf(v) + return encodeValue(val) +} + +func encodeValue(val reflect.Value) ([]byte, error) { + var b []byte + var err error + + if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + if val.IsNil() { + return []byte(""), nil + } + + val = val.Elem() + } + + switch val.Kind() { + case reflect.Struct: + switch val.Interface().(type) { + case time.Time: + t := val.Interface().(time.Time) + b = []byte(fmt.Sprintf("%s", t.Format(iso8601))) + default: + b, err = encodeStruct(val) + } + case reflect.Map: + b, err = encodeMap(val) + case reflect.Slice: + b, err = encodeSlice(val) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + b = []byte(fmt.Sprintf("%s", strconv.FormatInt(val.Int(), 10))) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + b = []byte(fmt.Sprintf("%s", strconv.FormatUint(val.Uint(), 10))) + case reflect.Float32, reflect.Float64: + b = []byte(fmt.Sprintf("%s", + strconv.FormatFloat(val.Float(), 'f', -1, val.Type().Bits()))) + case reflect.Bool: + if val.Bool() { + b = []byte("1") + } else { + b = []byte("0") + } + case reflect.String: + var buf bytes.Buffer + + xml.Escape(&buf, []byte(val.String())) + + if _, ok := val.Interface().(Base64); ok { + b = []byte(fmt.Sprintf("%s", buf.String())) + } else { + b = []byte(fmt.Sprintf("%s", buf.String())) + } + default: + return nil, fmt.Errorf("xmlrpc encode error: unsupported type") + } + + if err != nil { + return nil, err + } + + return []byte(fmt.Sprintf("%s", string(b))), nil +} + +func encodeStruct(val reflect.Value) ([]byte, error) { + var b bytes.Buffer + + b.WriteString("") + + t := val.Type() + for i := 0; i < t.NumField(); i++ { + b.WriteString("") + f := t.Field(i) + + name := f.Tag.Get("xmlrpc") + if name == "" { + name = f.Name + } + b.WriteString(fmt.Sprintf("%s", name)) + + p, err := encodeValue(val.FieldByName(f.Name)) + if err != nil { + return nil, err + } + b.Write(p) + + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} + +var sortMapKeys bool + +func encodeMap(val reflect.Value) ([]byte, error) { + var t = val.Type() + + if t.Key().Kind() != reflect.String { + return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported") + } + + var b bytes.Buffer + + b.WriteString("") + + keys := val.MapKeys() + + if sortMapKeys { + sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() }) + } + + for i := 0; i < val.Len(); i++ { + key := keys[i] + kval := val.MapIndex(key) + + b.WriteString("") + b.WriteString(fmt.Sprintf("%s", key.String())) + + p, err := encodeValue(kval) + + if err != nil { + return nil, err + } + + b.Write(p) + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} + +func encodeSlice(val reflect.Value) ([]byte, error) { + var b bytes.Buffer + + b.WriteString("") + + for i := 0; i < val.Len(); i++ { + p, err := encodeValue(val.Index(i)) + if err != nil { + return nil, err + } + + b.Write(p) + } + + b.WriteString("") + + return b.Bytes(), nil +} diff --git a/vendor/github.com/kolo/xmlrpc/encoder_test.go b/vendor/github.com/kolo/xmlrpc/encoder_test.go new file mode 100644 index 00000000..ca4ac706 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/encoder_test.go @@ -0,0 +1,58 @@ +package xmlrpc + +import ( + "testing" + "time" +) + +var marshalTests = []struct { + value interface{} + xml string +}{ + {100, "100"}, + {"Once upon a time", "Once upon a time"}, + {"Mike & Mick ", "Mike & Mick <London, UK>"}, + {Base64("T25jZSB1cG9uIGEgdGltZQ=="), "T25jZSB1cG9uIGEgdGltZQ=="}, + {true, "1"}, + {false, "0"}, + {12.134, "12.134"}, + {-12.134, "-12.134"}, + {738777323.0, "738777323"}, + {time.Unix(1386622812, 0).UTC(), "20131209T21:00:12"}, + {[]interface{}{1, "one"}, "1one"}, + {&struct { + Title string + Amount int + }{"War and Piece", 20}, "TitleWar and PieceAmount20"}, + {&struct { + Value interface{} `xmlrpc:"value"` + }{}, "value"}, + { + map[string]interface{}{"title": "War and Piece", "amount": 20}, + "amount20titleWar and Piece", + }, + { + map[string]interface{}{ + "Name": "John Smith", + "Age": 6, + "Wight": []float32{66.67, 100.5}, + "Dates": map[string]interface{}{"Birth": time.Date(1829, time.November, 10, 23, 0, 0, 0, time.UTC), "Death": time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}}, + "Age6DatesBirth18291110T23:00:00Death20091110T23:00:00NameJohn SmithWight66.67100.5", + }, +} + +func Test_marshal(t *testing.T) { + sortMapKeys = true + + for _, tt := range marshalTests { + b, err := marshal(tt.value) + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + if string(b) != tt.xml { + t.Fatalf("marshal error:\nexpected: %s\n got: %s", tt.xml, string(b)) + } + + } +} diff --git a/vendor/github.com/kolo/xmlrpc/fixtures/cp1251.xml b/vendor/github.com/kolo/xmlrpc/fixtures/cp1251.xml new file mode 100644 index 00000000..1d5e9bfc --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/fixtures/cp1251.xml @@ -0,0 +1,6 @@ + + + + .. - + + \ No newline at end of file diff --git a/vendor/github.com/kolo/xmlrpc/request.go b/vendor/github.com/kolo/xmlrpc/request.go new file mode 100644 index 00000000..acb8251b --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/request.go @@ -0,0 +1,57 @@ +package xmlrpc + +import ( + "bytes" + "fmt" + "net/http" +) + +func NewRequest(url string, method string, args interface{}) (*http.Request, error) { + var t []interface{} + var ok bool + if t, ok = args.([]interface{}); !ok { + if args != nil { + t = []interface{}{args} + } + } + + body, err := EncodeMethodCall(method, t...) + if err != nil { + return nil, err + } + + request, err := http.NewRequest("POST", url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + request.Header.Set("Content-Type", "text/xml") + request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body))) + + return request, nil +} + +func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) { + var b bytes.Buffer + b.WriteString(``) + b.WriteString(fmt.Sprintf("%s", method)) + + if args != nil { + b.WriteString("") + + for _, arg := range args { + p, err := marshal(arg) + if err != nil { + return nil, err + } + + b.WriteString(fmt.Sprintf("%s", string(p))) + } + + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} diff --git a/vendor/github.com/kolo/xmlrpc/response.go b/vendor/github.com/kolo/xmlrpc/response.go new file mode 100644 index 00000000..6742a1c7 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/response.go @@ -0,0 +1,52 @@ +package xmlrpc + +import ( + "regexp" +) + +var ( + faultRx = regexp.MustCompile(`(\s|\S)+`) +) + +type failedResponse struct { + Code int `xmlrpc:"faultCode"` + Error string `xmlrpc:"faultString"` +} + +func (r *failedResponse) err() error { + return &xmlrpcError{ + code: r.Code, + err: r.Error, + } +} + +type Response struct { + data []byte +} + +func NewResponse(data []byte) *Response { + return &Response{ + data: data, + } +} + +func (r *Response) Failed() bool { + return faultRx.Match(r.data) +} + +func (r *Response) Err() error { + failedResp := new(failedResponse) + if err := unmarshal(r.data, failedResp); err != nil { + return err + } + + return failedResp.err() +} + +func (r *Response) Unmarshal(v interface{}) error { + if err := unmarshal(r.data, v); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/kolo/xmlrpc/response_test.go b/vendor/github.com/kolo/xmlrpc/response_test.go new file mode 100644 index 00000000..55095c24 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/response_test.go @@ -0,0 +1,84 @@ +package xmlrpc + +import ( + "testing" +) + +const faultRespXml = ` + + + + + + + faultString + + You must log in before using this part of Bugzilla. + + + + faultCode + + 410 + + + + + +` + +func Test_failedResponse(t *testing.T) { + resp := NewResponse([]byte(faultRespXml)) + + if !resp.Failed() { + t.Fatal("Failed() error: expected true, got false") + } + + if resp.Err() == nil { + t.Fatal("Err() error: expected error, got nil") + } + + err := resp.Err().(*xmlrpcError) + if err.code != 410 && err.err != "You must log in before using this part of Bugzilla." { + t.Fatal("Err() error: got wrong error") + } +} + +const emptyValResp = ` + + + + + + + + user + Joe Smith + + + token + + + + + + +` + + +func Test_responseWithEmptyValue(t *testing.T) { + resp := NewResponse([]byte(emptyValResp)) + + result := struct{ + User string `xmlrpc:"user"` + Token string `xmlrpc:"token"` + }{} + + if err := resp.Unmarshal(&result); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + + if result.User != "Joe Smith" || result.Token != "" { + t.Fatalf("unexpected result: %v", result) + } +} diff --git a/vendor/github.com/kolo/xmlrpc/test_server.rb b/vendor/github.com/kolo/xmlrpc/test_server.rb new file mode 100644 index 00000000..1b1ff876 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/test_server.rb @@ -0,0 +1,25 @@ +# encoding: utf-8 + +require "xmlrpc/server" + +class Service + def time + Time.now + end + + def upcase(s) + s.upcase + end + + def sum(x, y) + x + y + end + + def error + raise XMLRPC::FaultException.new(500, "Server error") + end +end + +server = XMLRPC::Server.new 5001, 'localhost' +server.add_handler "service", Service.new +server.serve diff --git a/vendor/github.com/kolo/xmlrpc/xmlrpc.go b/vendor/github.com/kolo/xmlrpc/xmlrpc.go new file mode 100644 index 00000000..8766403a --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/xmlrpc.go @@ -0,0 +1,19 @@ +package xmlrpc + +import ( + "fmt" +) + +// xmlrpcError represents errors returned on xmlrpc request. +type xmlrpcError struct { + code int + err string +} + +// Error() method implements Error interface +func (e *xmlrpcError) Error() string { + return fmt.Sprintf("error: \"%s\" code: %d", e.err, e.code) +} + +// Base64 represents value in base64 encoding +type Base64 string