Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/snap: add debug api command #14116

Merged
merged 2 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,3 +801,9 @@ func (c *Client) MigrateSnapHome(snaps []string) (changeID string, err error) {

return c.doAsync("POST", "/v2/debug", nil, nil, bytes.NewReader(body))
}

// DebugRaw allows to make raw queries to the API with the intention of using it
// from the debug code.
func (client *Client) DebugRaw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
return client.raw(ctx, method, urlpath, query, headers, body)
}
144 changes: 144 additions & 0 deletions cmd/snap/cmd_debug_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think somewhere here it warrants a simple example just to show how it's used

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add an example (like you have in the PR description), you could add it to the command's long description so it's easily accessible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some examples in long description of the command.


/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package main

import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"strings"

"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/logger"
)

var longDebugAPIHelp = `
Execute a raw query to snapd API. Complex input can be read from stdin, while
output is printed to stdout. See examples below:

List all snaps:
$ snap debug api /v2/snaps

Find snaps with name foo:
$ snap debug api '/v2/find?name=foo'

Request refresh of snap 'some-snap':
$ echo '{"action": "refresh"}' | snap debug api -X POST \
-H 'Content-Type: application/json' /v2/snaps/some-snap
`

type cmdDebugAPI struct {
clientMixin

Headers []string `short:"H" long:"header"`
Method string `short:"X" long:"request"`
Fail bool `long:"fail"`
// TODO support -F/--form like curl
// TODO support -d/--data like curl

Positional struct {
PathAndQuery string `positional-arg-name:"<path-and-query>"`
} `positional-args:"yes" required:"yes"`
}

func init() {
addDebugCommand("api",
"Execute raw query to snapd API",
longDebugAPIHelp,
func() flags.Commander {
return &cmdDebugAPI{}
}, map[string]string{
"header": "Set header (can be repeated multiple times), header kind and value are separated with ': '",
"request": "HTTP method to use (defaults to GET)",
"fail": "Fail on request errors",
}, nil)
}

func (x *cmdDebugAPI) Execute(args []string) error {
method := x.Method
switch method {
case "GET", "POST", "PUT":
case "":
method = "GET"
default:
return fmt.Errorf("unsupported method %q", method)
}

var in io.Reader
switch method {
case "POST", "PUT":
in = Stdin
}

u, err := url.Parse(x.Positional.PathAndQuery)
if err != nil {
return err
}

hdrs := x.Headers
reqHdrs := make(map[string]string, len(hdrs))
for _, hdr := range x.Headers {
before, after, sep := strings.Cut(hdr, ": ")
if !sep {
return fmt.Errorf("cannot parse header %q", hdr)
}
reqHdrs[before] = after
}
logger.Debugf("url: %v", u.Path)
logger.Debugf("query: %v", u.RawQuery)
logger.Debugf("headers: %s", reqHdrs)

rsp, err := x.client.DebugRaw(context.Background(), method, u.Path, u.Query(), reqHdrs, in)
if err != nil {
return err
}
defer rsp.Body.Close()

logger.Debugf("response: %v", rsp.Status)
logger.Debugf("response headers: %v", rsp.Header)
logger.Debugf("response length: %v", rsp.ContentLength)

if rsp.Header.Get("Content-Type") == "application/json" {
// pretty print JSON response by default

var temp map[string]interface{}
if err := json.NewDecoder(rsp.Body).Decode(&temp); err != nil {
return err
}
enc := json.NewEncoder(Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(temp); err != nil {
return err
}
} else {
if _, err := io.Copy(Stdout, rsp.Body); err != nil {
return err
}
}

if x.Fail && rsp.StatusCode >= 400 {
// caller wants to fail on non success requests
return fmt.Errorf("request failed with status %v", rsp.Status)
}

return nil
}
Loading