github.com/rigado/snapd@v2.42.5-go-mod+incompatible/client/client.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package client
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"net"
    29  	"net/http"
    30  	"net/url"
    31  	"os"
    32  	"path"
    33  	"time"
    34  
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/jsonutil"
    37  )
    38  
    39  func unixDialer(socketPath string) func(string, string) (net.Conn, error) {
    40  	if socketPath == "" {
    41  		socketPath = dirs.SnapdSocket
    42  	}
    43  	return func(_, _ string) (net.Conn, error) {
    44  		return net.Dial("unix", socketPath)
    45  	}
    46  }
    47  
    48  type doer interface {
    49  	Do(*http.Request) (*http.Response, error)
    50  }
    51  
    52  // Config allows to customize client behavior.
    53  type Config struct {
    54  	// BaseURL contains the base URL where snappy daemon is expected to be.
    55  	// It can be empty for a default behavior of talking over a unix socket.
    56  	BaseURL string
    57  
    58  	// DisableAuth controls whether the client should send an
    59  	// Authorization header from reading the auth.json data.
    60  	DisableAuth bool
    61  
    62  	// Interactive controls whether the client runs in interactive mode.
    63  	// At present, this only affects whether interactive polkit
    64  	// authorisation is requested.
    65  	Interactive bool
    66  
    67  	// Socket is the path to the unix socket to use
    68  	Socket string
    69  
    70  	// DisableKeepAlive indicates whether the connections should not be kept
    71  	// alive for later reuse
    72  	DisableKeepAlive bool
    73  
    74  	// User-Agent to sent to the snapd daemon
    75  	UserAgent string
    76  }
    77  
    78  // A Client knows how to talk to the snappy daemon.
    79  type Client struct {
    80  	baseURL url.URL
    81  	doer    doer
    82  
    83  	disableAuth bool
    84  	interactive bool
    85  
    86  	maintenance error
    87  
    88  	warningCount     int
    89  	warningTimestamp time.Time
    90  
    91  	userAgent string
    92  }
    93  
    94  // New returns a new instance of Client
    95  func New(config *Config) *Client {
    96  	if config == nil {
    97  		config = &Config{}
    98  	}
    99  
   100  	// By default talk over an UNIX socket.
   101  	if config.BaseURL == "" {
   102  		transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive}
   103  		return &Client{
   104  			baseURL: url.URL{
   105  				Scheme: "http",
   106  				Host:   "localhost",
   107  			},
   108  			doer:        &http.Client{Transport: transport},
   109  			disableAuth: config.DisableAuth,
   110  			interactive: config.Interactive,
   111  			userAgent:   config.UserAgent,
   112  		}
   113  	}
   114  
   115  	baseURL, err := url.Parse(config.BaseURL)
   116  	if err != nil {
   117  		panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err))
   118  	}
   119  	return &Client{
   120  		baseURL:     *baseURL,
   121  		doer:        &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}},
   122  		disableAuth: config.DisableAuth,
   123  		interactive: config.Interactive,
   124  		userAgent:   config.UserAgent,
   125  	}
   126  }
   127  
   128  // Maintenance returns an error reflecting the daemon maintenance status or nil.
   129  func (client *Client) Maintenance() error {
   130  	return client.maintenance
   131  }
   132  
   133  // WarningsSummary returns the number of warnings that are ready to be shown to
   134  // the user, and the timestamp of the most recently added warning (useful for
   135  // silencing the warning alerts, and OKing the returned warnings).
   136  func (client *Client) WarningsSummary() (count int, timestamp time.Time) {
   137  	return client.warningCount, client.warningTimestamp
   138  }
   139  
   140  func (client *Client) WhoAmI() (string, error) {
   141  	user, err := readAuthData()
   142  	if os.IsNotExist(err) {
   143  		return "", nil
   144  	}
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	return user.Email, nil
   150  }
   151  
   152  func (client *Client) setAuthorization(req *http.Request) error {
   153  	user, err := readAuthData()
   154  	if os.IsNotExist(err) {
   155  		return nil
   156  	}
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	var buf bytes.Buffer
   162  	fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon)
   163  	for _, discharge := range user.Discharges {
   164  		fmt.Fprintf(&buf, `, discharge="%s"`, discharge)
   165  	}
   166  	req.Header.Set("Authorization", buf.String())
   167  	return nil
   168  }
   169  
   170  type RequestError struct{ error }
   171  
   172  func (e RequestError) Error() string {
   173  	return fmt.Sprintf("cannot build request: %v", e.error)
   174  }
   175  
   176  type AuthorizationError struct{ error }
   177  
   178  func (e AuthorizationError) Error() string {
   179  	return fmt.Sprintf("cannot add authorization: %v", e.error)
   180  }
   181  
   182  type ConnectionError struct{ error }
   183  
   184  func (e ConnectionError) Error() string {
   185  	return fmt.Sprintf("cannot communicate with server: %v", e.error)
   186  }
   187  
   188  // AllowInteractionHeader is the HTTP request header used to indicate
   189  // that the client is willing to allow interaction.
   190  const AllowInteractionHeader = "X-Allow-Interaction"
   191  
   192  // raw performs a request and returns the resulting http.Response and
   193  // error you usually only need to call this directly if you expect the
   194  // response to not be JSON, otherwise you'd call Do(...) instead.
   195  func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
   196  	// fake a url to keep http.Client happy
   197  	u := client.baseURL
   198  	u.Path = path.Join(client.baseURL.Path, urlpath)
   199  	u.RawQuery = query.Encode()
   200  	req, err := http.NewRequest(method, u.String(), body)
   201  	if err != nil {
   202  		return nil, RequestError{err}
   203  	}
   204  	if client.userAgent != "" {
   205  		req.Header.Set("User-Agent", client.userAgent)
   206  	}
   207  
   208  	for key, value := range headers {
   209  		req.Header.Set(key, value)
   210  	}
   211  
   212  	if !client.disableAuth {
   213  		// set Authorization header if there are user's credentials
   214  		err = client.setAuthorization(req)
   215  		if err != nil {
   216  			return nil, AuthorizationError{err}
   217  		}
   218  	}
   219  
   220  	if client.interactive {
   221  		req.Header.Set(AllowInteractionHeader, "true")
   222  	}
   223  
   224  	rsp, err := client.doer.Do(req)
   225  	if err != nil {
   226  		return nil, ConnectionError{err}
   227  	}
   228  
   229  	return rsp, nil
   230  }
   231  
   232  var (
   233  	doRetry   = 250 * time.Millisecond
   234  	doTimeout = 5 * time.Second
   235  )
   236  
   237  // MockDoRetry mocks the delays used by the do retry loop.
   238  func MockDoRetry(retry, timeout time.Duration) (restore func()) {
   239  	oldRetry := doRetry
   240  	oldTimeout := doTimeout
   241  	doRetry = retry
   242  	doTimeout = timeout
   243  	return func() {
   244  		doRetry = oldRetry
   245  		doTimeout = oldTimeout
   246  	}
   247  }
   248  
   249  type hijacked struct {
   250  	do func(*http.Request) (*http.Response, error)
   251  }
   252  
   253  func (h hijacked) Do(req *http.Request) (*http.Response, error) {
   254  	return h.do(req)
   255  }
   256  
   257  // Hijack lets the caller take over the raw http request
   258  func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) {
   259  	client.doer = hijacked{f}
   260  }
   261  
   262  // do performs a request and decodes the resulting json into the given
   263  // value. It's low-level, for testing/experimenting only; you should
   264  // usually use a higher level interface that builds on this.
   265  func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (statusCode int, err error) {
   266  	retry := time.NewTicker(doRetry)
   267  	defer retry.Stop()
   268  	timeout := time.After(doTimeout)
   269  	var rsp *http.Response
   270  	for {
   271  		rsp, err = client.raw(method, path, query, headers, body)
   272  		if err == nil || method != "GET" {
   273  			break
   274  		}
   275  		select {
   276  		case <-retry.C:
   277  			continue
   278  		case <-timeout:
   279  		}
   280  		break
   281  	}
   282  	if err != nil {
   283  		return 0, err
   284  	}
   285  	defer rsp.Body.Close()
   286  
   287  	if v != nil {
   288  		if err := decodeInto(rsp.Body, v); err != nil {
   289  			return rsp.StatusCode, err
   290  		}
   291  	}
   292  
   293  	return rsp.StatusCode, nil
   294  }
   295  
   296  func decodeInto(reader io.Reader, v interface{}) error {
   297  	dec := json.NewDecoder(reader)
   298  	if err := dec.Decode(v); err != nil {
   299  		r := dec.Buffered()
   300  		buf, err1 := ioutil.ReadAll(r)
   301  		if err1 != nil {
   302  			buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1))
   303  		}
   304  		return fmt.Errorf("cannot decode %q: %s", buf, err)
   305  	}
   306  	return nil
   307  }
   308  
   309  // doSync performs a request to the given path using the specified HTTP method.
   310  // It expects a "sync" response from the API and on success decodes the JSON
   311  // response payload into the given value using the "UseNumber" json decoding
   312  // which produces json.Numbers instead of float64 types for numbers.
   313  func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) {
   314  	var rsp response
   315  	statusCode, err := client.do(method, path, query, headers, body, &rsp)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	if err := rsp.err(client, statusCode); err != nil {
   320  		return nil, err
   321  	}
   322  	if rsp.Type != "sync" {
   323  		return nil, fmt.Errorf("expected sync response, got %q", rsp.Type)
   324  	}
   325  
   326  	if v != nil {
   327  		if err := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil {
   328  			return nil, fmt.Errorf("cannot unmarshal: %v", err)
   329  		}
   330  	}
   331  
   332  	client.warningCount = rsp.WarningCount
   333  	client.warningTimestamp = rsp.WarningTimestamp
   334  
   335  	return &rsp.ResultInfo, nil
   336  }
   337  
   338  func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) {
   339  	_, changeID, err = client.doAsyncFull(method, path, query, headers, body)
   340  	return
   341  }
   342  
   343  func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) {
   344  	var rsp response
   345  	statusCode, err := client.do(method, path, query, headers, body, &rsp)
   346  	if err != nil {
   347  		return nil, "", err
   348  	}
   349  	if err := rsp.err(client, statusCode); err != nil {
   350  		return nil, "", err
   351  	}
   352  	if rsp.Type != "async" {
   353  		return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type)
   354  	}
   355  	if statusCode != 202 {
   356  		return nil, "", fmt.Errorf("operation not accepted")
   357  	}
   358  	if rsp.Change == "" {
   359  		return nil, "", fmt.Errorf("async response without change reference")
   360  	}
   361  
   362  	return rsp.Result, rsp.Change, nil
   363  }
   364  
   365  type ServerVersion struct {
   366  	Version     string
   367  	Series      string
   368  	OSID        string
   369  	OSVersionID string
   370  	OnClassic   bool
   371  
   372  	KernelVersion string
   373  }
   374  
   375  func (client *Client) ServerVersion() (*ServerVersion, error) {
   376  	sysInfo, err := client.SysInfo()
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	return &ServerVersion{
   382  		Version:     sysInfo.Version,
   383  		Series:      sysInfo.Series,
   384  		OSID:        sysInfo.OSRelease.ID,
   385  		OSVersionID: sysInfo.OSRelease.VersionID,
   386  		OnClassic:   sysInfo.OnClassic,
   387  
   388  		KernelVersion: sysInfo.KernelVersion,
   389  	}, nil
   390  }
   391  
   392  // A response produced by the REST API will usually fit in this
   393  // (exceptions are the icons/ endpoints obvs)
   394  type response struct {
   395  	Result json.RawMessage `json:"result"`
   396  	Type   string          `json:"type"`
   397  	Change string          `json:"change"`
   398  
   399  	WarningCount     int       `json:"warning-count"`
   400  	WarningTimestamp time.Time `json:"warning-timestamp"`
   401  
   402  	ResultInfo
   403  
   404  	Maintenance *Error `json:"maintenance"`
   405  }
   406  
   407  // Error is the real value of response.Result when an error occurs.
   408  type Error struct {
   409  	Kind    string      `json:"kind"`
   410  	Value   interface{} `json:"value"`
   411  	Message string      `json:"message"`
   412  
   413  	StatusCode int
   414  }
   415  
   416  func (e *Error) Error() string {
   417  	return e.Message
   418  }
   419  
   420  const (
   421  	ErrorKindTwoFactorRequired = "two-factor-required"
   422  	ErrorKindTwoFactorFailed   = "two-factor-failed"
   423  	ErrorKindLoginRequired     = "login-required"
   424  	ErrorKindInvalidAuthData   = "invalid-auth-data"
   425  	ErrorKindTermsNotAccepted  = "terms-not-accepted"
   426  	ErrorKindNoPaymentMethods  = "no-payment-methods"
   427  	ErrorKindPaymentDeclined   = "payment-declined"
   428  	ErrorKindPasswordPolicy    = "password-policy"
   429  
   430  	ErrorKindSnapAlreadyInstalled   = "snap-already-installed"
   431  	ErrorKindSnapNotInstalled       = "snap-not-installed"
   432  	ErrorKindSnapNotFound           = "snap-not-found"
   433  	ErrorKindAppNotFound            = "app-not-found"
   434  	ErrorKindSnapLocal              = "snap-local"
   435  	ErrorKindSnapNeedsDevMode       = "snap-needs-devmode"
   436  	ErrorKindSnapNeedsClassic       = "snap-needs-classic"
   437  	ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system"
   438  	ErrorKindSnapNotClassic         = "snap-not-classic"
   439  	ErrorKindNoUpdateAvailable      = "snap-no-update-available"
   440  
   441  	ErrorKindRevisionNotAvailable     = "snap-revision-not-available"
   442  	ErrorKindChannelNotAvailable      = "snap-channel-not-available"
   443  	ErrorKindArchitectureNotAvailable = "snap-architecture-not-available"
   444  
   445  	ErrorKindChangeConflict = "snap-change-conflict"
   446  
   447  	ErrorKindNotSnap = "snap-not-a-snap"
   448  
   449  	ErrorKindNetworkTimeout = "network-timeout"
   450  	ErrorKindDNSFailure     = "dns-failure"
   451  
   452  	ErrorKindInterfacesUnchanged = "interfaces-unchanged"
   453  
   454  	ErrorKindBadQuery           = "bad-query"
   455  	ErrorKindConfigNoSuchOption = "option-not-found"
   456  
   457  	ErrorKindSystemRestart = "system-restart"
   458  	ErrorKindDaemonRestart = "daemon-restart"
   459  
   460  	ErrorKindAssertionNotFound = "assertion-not-found"
   461  )
   462  
   463  // IsRetryable returns true if the given error is an error
   464  // that can be retried later.
   465  func IsRetryable(err error) bool {
   466  	switch e := err.(type) {
   467  	case *Error:
   468  		return e.Kind == ErrorKindChangeConflict
   469  	}
   470  	return false
   471  }
   472  
   473  // IsTwoFactorError returns whether the given error is due to problems
   474  // in two-factor authentication.
   475  func IsTwoFactorError(err error) bool {
   476  	e, ok := err.(*Error)
   477  	if !ok || e == nil {
   478  		return false
   479  	}
   480  
   481  	return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired
   482  }
   483  
   484  // IsInterfacesUnchangedError returns whether the given error means the requested
   485  // change to interfaces was not made, because there was nothing to do.
   486  func IsInterfacesUnchangedError(err error) bool {
   487  	e, ok := err.(*Error)
   488  	if !ok || e == nil {
   489  		return false
   490  	}
   491  	return e.Kind == ErrorKindInterfacesUnchanged
   492  }
   493  
   494  // IsAssertionNotFoundError returns whether the given error means that the
   495  // assertion wasn't found and thus the device isn't ready/seeded.
   496  func IsAssertionNotFoundError(err error) bool {
   497  	e, ok := err.(*Error)
   498  	if !ok || e == nil {
   499  		return false
   500  	}
   501  
   502  	return e.Kind == ErrorKindAssertionNotFound
   503  }
   504  
   505  // OSRelease contains information about the system extracted from /etc/os-release.
   506  type OSRelease struct {
   507  	ID        string `json:"id"`
   508  	VersionID string `json:"version-id,omitempty"`
   509  }
   510  
   511  // RefreshInfo contains information about refreshes.
   512  type RefreshInfo struct {
   513  	// Timer contains the refresh.timer setting.
   514  	Timer string `json:"timer,omitempty"`
   515  	// Schedule contains the legacy refresh.schedule setting.
   516  	Schedule string `json:"schedule,omitempty"`
   517  	Last     string `json:"last,omitempty"`
   518  	Hold     string `json:"hold,omitempty"`
   519  	Next     string `json:"next,omitempty"`
   520  }
   521  
   522  // SysInfo holds system information
   523  type SysInfo struct {
   524  	Series    string    `json:"series,omitempty"`
   525  	Version   string    `json:"version,omitempty"`
   526  	BuildID   string    `json:"build-id"`
   527  	OSRelease OSRelease `json:"os-release"`
   528  	OnClassic bool      `json:"on-classic"`
   529  	Managed   bool      `json:"managed"`
   530  
   531  	KernelVersion string `json:"kernel-version,omitempty"`
   532  
   533  	Refresh         RefreshInfo         `json:"refresh,omitempty"`
   534  	Confinement     string              `json:"confinement"`
   535  	SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"`
   536  }
   537  
   538  func (rsp *response) err(cli *Client, statusCode int) error {
   539  	if cli != nil {
   540  		maintErr := rsp.Maintenance
   541  		// avoid setting to (*client.Error)(nil)
   542  		if maintErr != nil {
   543  			cli.maintenance = maintErr
   544  		} else {
   545  			cli.maintenance = nil
   546  		}
   547  	}
   548  	if rsp.Type != "error" {
   549  		return nil
   550  	}
   551  	var resultErr Error
   552  	err := json.Unmarshal(rsp.Result, &resultErr)
   553  	if err != nil || resultErr.Message == "" {
   554  		return fmt.Errorf("server error: %q", http.StatusText(statusCode))
   555  	}
   556  	resultErr.StatusCode = statusCode
   557  
   558  	return &resultErr
   559  }
   560  
   561  func parseError(r *http.Response) error {
   562  	var rsp response
   563  	if r.Header.Get("Content-Type") != "application/json" {
   564  		return fmt.Errorf("server error: %q", r.Status)
   565  	}
   566  
   567  	dec := json.NewDecoder(r.Body)
   568  	if err := dec.Decode(&rsp); err != nil {
   569  		return fmt.Errorf("cannot unmarshal error: %v", err)
   570  	}
   571  
   572  	err := rsp.err(nil, r.StatusCode)
   573  	if err == nil {
   574  		return fmt.Errorf("server error: %q", r.Status)
   575  	}
   576  	return err
   577  }
   578  
   579  // SysInfo gets system information from the REST API.
   580  func (client *Client) SysInfo() (*SysInfo, error) {
   581  	var sysInfo SysInfo
   582  
   583  	if _, err := client.doSync("GET", "/v2/system-info", nil, nil, nil, &sysInfo); err != nil {
   584  		return nil, fmt.Errorf("cannot obtain system details: %v", err)
   585  	}
   586  
   587  	return &sysInfo, nil
   588  }
   589  
   590  // CreateUserResult holds the result of a user creation.
   591  type CreateUserResult struct {
   592  	Username string   `json:"username"`
   593  	SSHKeys  []string `json:"ssh-keys"`
   594  }
   595  
   596  // CreateUserOptions holds options for creating a local system user.
   597  //
   598  // If Known is false, the provided email is used to query the store for
   599  // username and SSH key details.
   600  //
   601  // If Known is true, the user will be created by looking through existing
   602  // system-user assertions and looking for a matching email. If Email is
   603  // empty then all such assertions are considered and multiple users may
   604  // be created.
   605  type CreateUserOptions struct {
   606  	Email        string `json:"email,omitempty"`
   607  	Sudoer       bool   `json:"sudoer,omitempty"`
   608  	Known        bool   `json:"known,omitempty"`
   609  	ForceManaged bool   `json:"force-managed,omitempty"`
   610  }
   611  
   612  // CreateUser creates a local system user. See CreateUserOptions for details.
   613  func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) {
   614  	if options.Email == "" {
   615  		return nil, fmt.Errorf("cannot create a user without providing an email")
   616  	}
   617  
   618  	var result CreateUserResult
   619  	data, err := json.Marshal(options)
   620  	if err != nil {
   621  		return nil, err
   622  	}
   623  
   624  	if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
   625  		return nil, fmt.Errorf("while creating user: %v", err)
   626  	}
   627  	return &result, nil
   628  }
   629  
   630  // CreateUsers creates multiple local system users. See CreateUserOptions for details.
   631  //
   632  // Results may be provided even if there are errors.
   633  func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) {
   634  	for _, opts := range options {
   635  		if opts.Email == "" && !opts.Known {
   636  			return nil, fmt.Errorf("cannot create user from store details without an email to query for")
   637  		}
   638  	}
   639  
   640  	var results []*CreateUserResult
   641  	var errs []error
   642  
   643  	for _, opts := range options {
   644  		data, err := json.Marshal(opts)
   645  		if err != nil {
   646  			return nil, err
   647  		}
   648  
   649  		if opts.Email == "" {
   650  			var result []*CreateUserResult
   651  			if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
   652  				errs = append(errs, err)
   653  			} else {
   654  				results = append(results, result...)
   655  			}
   656  		} else {
   657  			var result *CreateUserResult
   658  			if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
   659  				errs = append(errs, err)
   660  			} else {
   661  				results = append(results, result)
   662  			}
   663  		}
   664  	}
   665  
   666  	if len(errs) == 1 {
   667  		return results, errs[0]
   668  	}
   669  	if len(errs) > 1 {
   670  		var buf bytes.Buffer
   671  		for _, err := range errs {
   672  			fmt.Fprintf(&buf, "\n- %s", err)
   673  		}
   674  		return results, fmt.Errorf("while creating users:%s", buf.Bytes())
   675  	}
   676  	return results, nil
   677  }
   678  
   679  // Users returns the local users.
   680  func (client *Client) Users() ([]*User, error) {
   681  	var result []*User
   682  
   683  	if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil {
   684  		return nil, fmt.Errorf("while getting users: %v", err)
   685  	}
   686  	return result, nil
   687  }
   688  
   689  type debugAction struct {
   690  	Action string      `json:"action"`
   691  	Params interface{} `json:"params,omitempty"`
   692  }
   693  
   694  // Debug is only useful when writing test code, it will trigger
   695  // an internal action with the given parameters.
   696  func (client *Client) Debug(action string, params interface{}, result interface{}) error {
   697  	body, err := json.Marshal(debugAction{
   698  		Action: action,
   699  		Params: params,
   700  	})
   701  	if err != nil {
   702  		return err
   703  	}
   704  
   705  	_, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result)
   706  	return err
   707  }
   708  
   709  func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error {
   710  	urlParams := url.Values{"aspect": []string{aspect}}
   711  	for k, v := range params {
   712  		urlParams.Set(k, v)
   713  	}
   714  	_, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result)
   715  	return err
   716  }