From 73aa51e86e111cf64a290ebb8c079ed130149eec Mon Sep 17 00:00:00 2001 From: Paul Rodriguez Date: Fri, 4 Aug 2023 10:49:16 +0200 Subject: [PATCH] Support daemon API changes from sister projects (#265) --- internals/daemon/api.go | 4 +-- internals/daemon/api_changes.go | 8 +++--- internals/daemon/api_checks.go | 2 +- internals/daemon/api_exec.go | 2 +- internals/daemon/api_files.go | 4 +-- internals/daemon/api_health.go | 2 +- internals/daemon/api_logs.go | 2 +- internals/daemon/api_plan.go | 4 +-- internals/daemon/api_services.go | 8 +++--- internals/daemon/api_signals.go | 2 +- internals/daemon/api_tasks.go | 2 +- internals/daemon/api_test.go | 2 +- internals/daemon/api_warnings.go | 4 +-- internals/daemon/daemon.go | 15 ++++++----- internals/daemon/daemon_test.go | 45 +++++++++++++++++++++++++------- 15 files changed, 68 insertions(+), 38 deletions(-) diff --git a/internals/daemon/api.go b/internals/daemon/api.go index 565b9002..e29a53e0 100644 --- a/internals/daemon/api.go +++ b/internals/daemon/api.go @@ -24,7 +24,7 @@ import ( "github.com/canonical/pebble/internals/overlord/state" ) -var api = []*Command{{ +var API = []*Command{{ // See daemon.go:canAccess for details how the access is controlled. Path: "/v1/system-info", GuestOK: true, @@ -107,7 +107,7 @@ var ( muxVars = mux.Vars ) -func v1SystemInfo(c *Command, r *http.Request, _ *userState) Response { +func v1SystemInfo(c *Command, r *http.Request, _ *UserState) Response { state := c.d.overlord.State() state.Lock() defer state.Unlock() diff --git a/internals/daemon/api_changes.go b/internals/daemon/api_changes.go index 6705cc50..55bba5d4 100644 --- a/internals/daemon/api_changes.go +++ b/internals/daemon/api_changes.go @@ -115,7 +115,7 @@ func change2changeInfo(chg *state.Change) *changeInfo { return chgInfo } -func v1GetChanges(c *Command, r *http.Request, _ *userState) Response { +func v1GetChanges(c *Command, r *http.Request, _ *UserState) Response { query := r.URL.Query() qselect := query.Get("select") if qselect == "" { @@ -170,7 +170,7 @@ func v1GetChanges(c *Command, r *http.Request, _ *userState) Response { return SyncResponse(chgInfos) } -func v1GetChange(c *Command, r *http.Request, _ *userState) Response { +func v1GetChange(c *Command, r *http.Request, _ *UserState) Response { changeID := muxVars(r)["id"] st := c.d.overlord.State() st.Lock() @@ -183,7 +183,7 @@ func v1GetChange(c *Command, r *http.Request, _ *userState) Response { return SyncResponse(change2changeInfo(chg)) } -func v1GetChangeWait(c *Command, r *http.Request, _ *userState) Response { +func v1GetChangeWait(c *Command, r *http.Request, _ *UserState) Response { changeID := muxVars(r)["id"] st := c.d.overlord.State() st.Lock() @@ -224,7 +224,7 @@ func v1GetChangeWait(c *Command, r *http.Request, _ *userState) Response { return SyncResponse(change2changeInfo(change)) } -func v1PostChange(c *Command, r *http.Request, _ *userState) Response { +func v1PostChange(c *Command, r *http.Request, _ *UserState) Response { chID := muxVars(r)["id"] state := c.d.overlord.State() state.Lock() diff --git a/internals/daemon/api_checks.go b/internals/daemon/api_checks.go index d8588fc2..da7e0a61 100644 --- a/internals/daemon/api_checks.go +++ b/internals/daemon/api_checks.go @@ -30,7 +30,7 @@ type checkInfo struct { Threshold int `json:"threshold"` } -func v1GetChecks(c *Command, r *http.Request, _ *userState) Response { +func v1GetChecks(c *Command, r *http.Request, _ *UserState) Response { query := r.URL.Query() level := plan.CheckLevel(query.Get("level")) switch level { diff --git a/internals/daemon/api_exec.go b/internals/daemon/api_exec.go index eab7bd43..7318f3e8 100644 --- a/internals/daemon/api_exec.go +++ b/internals/daemon/api_exec.go @@ -44,7 +44,7 @@ type execPayload struct { Height int `json:"height"` } -func v1PostExec(c *Command, req *http.Request, _ *userState) Response { +func v1PostExec(c *Command, req *http.Request, _ *UserState) Response { var payload execPayload decoder := json.NewDecoder(req.Body) if err := decoder.Decode(&payload); err != nil { diff --git a/internals/daemon/api_files.go b/internals/daemon/api_files.go index bf8062db..21e9db03 100644 --- a/internals/daemon/api_files.go +++ b/internals/daemon/api_files.go @@ -37,7 +37,7 @@ import ( const minBoundaryLength = 32 -func v1GetFiles(_ *Command, req *http.Request, _ *userState) Response { +func v1GetFiles(_ *Command, req *http.Request, _ *UserState) Response { query := req.URL.Query() action := query.Get("action") switch action { @@ -337,7 +337,7 @@ func listFiles(path, pattern string, itself bool) ([]fileInfoResult, error) { return result, nil } -func v1PostFiles(_ *Command, req *http.Request, _ *userState) Response { +func v1PostFiles(_ *Command, req *http.Request, _ *UserState) Response { contentType := req.Header.Get("Content-Type") mediaType, params, err := mime.ParseMediaType(contentType) if err != nil { diff --git a/internals/daemon/api_health.go b/internals/daemon/api_health.go index 3b1c3960..ffcab732 100644 --- a/internals/daemon/api_health.go +++ b/internals/daemon/api_health.go @@ -28,7 +28,7 @@ type healthInfo struct { Healthy bool `json:"healthy"` } -func v1Health(c *Command, r *http.Request, _ *userState) Response { +func v1Health(c *Command, r *http.Request, _ *UserState) Response { query := r.URL.Query() level := plan.CheckLevel(query.Get("level")) switch level { diff --git a/internals/daemon/api_logs.go b/internals/daemon/api_logs.go index 7dc0354f..46439aff 100644 --- a/internals/daemon/api_logs.go +++ b/internals/daemon/api_logs.go @@ -42,7 +42,7 @@ type serviceManager interface { ServiceLogs(services []string, last int) (map[string]servicelog.Iterator, error) } -func v1GetLogs(c *Command, _ *http.Request, _ *userState) Response { +func v1GetLogs(c *Command, _ *http.Request, _ *UserState) Response { return logsResponse{ svcMgr: overlordServiceManager(c.d.overlord), } diff --git a/internals/daemon/api_plan.go b/internals/daemon/api_plan.go index 0d765f83..3d1068dd 100644 --- a/internals/daemon/api_plan.go +++ b/internals/daemon/api_plan.go @@ -24,7 +24,7 @@ import ( "github.com/canonical/pebble/internals/plan" ) -func v1GetPlan(c *Command, r *http.Request, _ *userState) Response { +func v1GetPlan(c *Command, r *http.Request, _ *UserState) Response { format := r.URL.Query().Get("format") if format != "yaml" { return statusBadRequest("invalid format %q", format) @@ -42,7 +42,7 @@ func v1GetPlan(c *Command, r *http.Request, _ *userState) Response { return SyncResponse(string(planYAML)) } -func v1PostLayers(c *Command, r *http.Request, _ *userState) Response { +func v1PostLayers(c *Command, r *http.Request, _ *UserState) Response { var payload struct { Action string `json:"action"` Combine bool `json:"combine"` diff --git a/internals/daemon/api_services.go b/internals/daemon/api_services.go index 961204db..fdfe13e0 100644 --- a/internals/daemon/api_services.go +++ b/internals/daemon/api_services.go @@ -35,7 +35,7 @@ type serviceInfo struct { CurrentSince *time.Time `json:"current-since,omitempty"` // pointer as omitempty doesn't work with time.Time directly } -func v1GetServices(c *Command, r *http.Request, _ *userState) Response { +func v1GetServices(c *Command, r *http.Request, _ *UserState) Response { names := strutil.MultiCommaSeparatedList(r.URL.Query()["names"]) servmgr := overlordServiceManager(c.d.overlord) @@ -59,7 +59,7 @@ func v1GetServices(c *Command, r *http.Request, _ *userState) Response { return SyncResponse(infos) } -func v1PostServices(c *Command, r *http.Request, _ *userState) Response { +func v1PostServices(c *Command, r *http.Request, _ *UserState) Response { var payload struct { Action string `json:"action"` Services []string `json:"services"` @@ -212,11 +212,11 @@ func v1PostServices(c *Command, r *http.Request, _ *userState) Response { return AsyncResponse(nil, change.ID()) } -func v1GetService(c *Command, r *http.Request, _ *userState) Response { +func v1GetService(c *Command, r *http.Request, _ *UserState) Response { return statusBadRequest("not implemented") } -func v1PostService(c *Command, r *http.Request, _ *userState) Response { +func v1PostService(c *Command, r *http.Request, _ *UserState) Response { return statusBadRequest("not implemented") } diff --git a/internals/daemon/api_signals.go b/internals/daemon/api_signals.go index 5be72e3b..81a32871 100644 --- a/internals/daemon/api_signals.go +++ b/internals/daemon/api_signals.go @@ -24,7 +24,7 @@ type signalsPayload struct { Services []string `json:"services"` } -func v1PostSignals(c *Command, req *http.Request, _ *userState) Response { +func v1PostSignals(c *Command, req *http.Request, _ *UserState) Response { var payload signalsPayload decoder := json.NewDecoder(req.Body) if err := decoder.Decode(&payload); err != nil { diff --git a/internals/daemon/api_tasks.go b/internals/daemon/api_tasks.go index bdc0721c..d362b7be 100644 --- a/internals/daemon/api_tasks.go +++ b/internals/daemon/api_tasks.go @@ -23,7 +23,7 @@ import ( "github.com/canonical/pebble/internals/overlord/state" ) -func v1GetTaskWebsocket(c *Command, req *http.Request, _ *userState) Response { +func v1GetTaskWebsocket(c *Command, req *http.Request, _ *UserState) Response { vars := muxVars(req) taskID := vars["task-id"] websocketID := vars["websocket-id"] diff --git a/internals/daemon/api_test.go b/internals/daemon/api_test.go index ed7ebd7f..c7b624d9 100644 --- a/internals/daemon/api_test.go +++ b/internals/daemon/api_test.go @@ -64,7 +64,7 @@ func (s *apiSuite) daemon(c *check.C) *Daemon { } func apiCmd(path string) *Command { - for _, cmd := range api { + for _, cmd := range API { if cmd.Path == path { return cmd } diff --git a/internals/daemon/api_warnings.go b/internals/daemon/api_warnings.go index 26718375..74a11e15 100644 --- a/internals/daemon/api_warnings.go +++ b/internals/daemon/api_warnings.go @@ -22,7 +22,7 @@ import ( "github.com/canonical/pebble/internals/overlord/state" ) -func v1AckWarnings(c *Command, r *http.Request, _ *userState) Response { +func v1AckWarnings(c *Command, r *http.Request, _ *UserState) Response { defer r.Body.Close() var op struct { Action string `json:"action"` @@ -43,7 +43,7 @@ func v1AckWarnings(c *Command, r *http.Request, _ *userState) Response { return SyncResponse(n) } -func v1GetWarnings(c *Command, r *http.Request, _ *userState) Response { +func v1GetWarnings(c *Command, r *http.Request, _ *UserState) Response { query := r.URL.Query() var all bool sel := query.Get("select") diff --git a/internals/daemon/daemon.go b/internals/daemon/daemon.go index 25455e24..5782f974 100644 --- a/internals/daemon/daemon.go +++ b/internals/daemon/daemon.go @@ -107,11 +107,14 @@ type Daemon struct { mu sync.Mutex } -// XXX Placeholder for now. -type userState struct{} +// UserState represents the state of an authenticated API user. +// +// The struct is currently empty as the behaviors haven't been implemented +// yet. +type UserState struct{} // A ResponseFunc handles one of the individual verbs for a method -type ResponseFunc func(*Command, *http.Request, *userState) Response +type ResponseFunc func(*Command, *http.Request, *UserState) Response // A Command routes a request to an individual per-verb ResponseFUnc type Command struct { @@ -149,7 +152,7 @@ const ( // - UserOK: any uid on the local system can access GET // - AdminOnly: only the administrator can access this // - UntrustedOK: can access this via the untrusted socket -func (c *Command) canAccess(r *http.Request, user *userState) accessResult { +func (c *Command) canAccess(r *http.Request, user *UserState) accessResult { if c.AdminOnly && (c.UserOK || c.GuestOK || c.UntrustedOK) { logger.Panicf("internal error: command cannot have AdminOnly together with any *OK flag") } @@ -210,7 +213,7 @@ func (c *Command) canAccess(r *http.Request, user *userState) accessResult { return accessUnauthorized } -func userFromRequest(state interface{}, r *http.Request) (*userState, error) { +func userFromRequest(state interface{}, r *http.Request) (*UserState, error) { return nil, nil } @@ -401,7 +404,7 @@ func (d *Daemon) SetDegradedMode(err error) { func (d *Daemon) addRoutes() { d.router = mux.NewRouter() - for _, c := range api { + for _, c := range API { c.d = d if c.PathPrefix == "" { d.router.Handle(c.Path, c).Name(c.Path) diff --git a/internals/daemon/daemon_test.go b/internals/daemon/daemon_test.go index 6a8e120f..c31f2cc5 100644 --- a/internals/daemon/daemon_test.go +++ b/internals/daemon/daemon_test.go @@ -99,6 +99,33 @@ func (h *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.lastMethod = r.Method } +func (s *daemonSuite) TestAddCommand(c *C) { + const endpoint = "/v1/addedendpoint" + var handler fakeHandler + getCallback := func(c *Command, r *http.Request, s *UserState) Response { + handler.cmd = c + return &handler + } + command := Command{ + Path: endpoint, + GuestOK: true, + GET: getCallback, + } + API = append(API, &command) + defer func() { + c.Assert(API[len(API)-1], Equals, &command) + API = API[:len(API)-1] + }() + + d := s.newDaemon(c) + d.Init() + d.Start() + defer d.Stop(nil) + + result := d.router.Get(endpoint).GetHandler() + c.Assert(result, Equals, &command) +} + func (s *daemonSuite) TestExplicitPaths(c *C) { s.socketPath = filepath.Join(c.MkDir(), "custom.socket") @@ -121,7 +148,7 @@ func (s *daemonSuite) TestCommandMethodDispatch(c *C) { cmd := &Command{d: s.newDaemon(c)} handler := &fakeHandler{cmd: cmd} - rf := func(innerCmd *Command, req *http.Request, user *userState) Response { + rf := func(innerCmd *Command, req *http.Request, user *UserState) Response { c.Assert(cmd, Equals, innerCmd) return handler } @@ -160,7 +187,7 @@ func (s *daemonSuite) TestCommandRestartingState(c *C) { d := s.newDaemon(c) cmd := &Command{d: d} - cmd.GET = func(*Command, *http.Request, *userState) Response { + cmd.GET = func(*Command, *http.Request, *UserState) Response { return SyncResponse(nil) } req, err := http.NewRequest("GET", "", nil) @@ -210,7 +237,7 @@ func (s *daemonSuite) TestFillsWarnings(c *C) { d := s.newDaemon(c) cmd := &Command{d: d} - cmd.GET = func(*Command, *http.Request, *userState) Response { + cmd.GET = func(*Command, *http.Request, *UserState) Response { return SyncResponse(nil) } req, err := http.NewRequest("GET", "", nil) @@ -341,7 +368,7 @@ func (s *daemonSuite) TestUserAccess(c *C) { func (s *daemonSuite) TestLoggedInUserAccess(c *C) { d := s.newDaemon(c) - user := &userState{} + user := &UserState{} get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;socket=;"} put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;socket=;"} @@ -399,8 +426,8 @@ func (s *daemonSuite) TestSuperAccess(c *C) { func (s *daemonSuite) TestAddRoutes(c *C) { d := s.newDaemon(c) - expected := make([]string, len(api)) - for i, v := range api { + expected := make([]string, len(API)) + for i, v := range API { if v.PathPrefix != "" { expected[i] = v.PathPrefix continue @@ -408,7 +435,7 @@ func (s *daemonSuite) TestAddRoutes(c *C) { expected[i] = v.Path } - got := make([]string, 0, len(api)) + got := make([]string, 0, len(API)) c.Assert(d.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { got = append(got, route.GetName()) return nil @@ -1028,10 +1055,10 @@ func doTestReq(c *C, cmd *Command, mth string) *httptest.ResponseRecorder { func (s *daemonSuite) TestDegradedModeReply(c *C) { d := s.newDaemon(c) cmd := &Command{d: d} - cmd.GET = func(*Command, *http.Request, *userState) Response { + cmd.GET = func(*Command, *http.Request, *UserState) Response { return SyncResponse(nil) } - cmd.POST = func(*Command, *http.Request, *userState) Response { + cmd.POST = func(*Command, *http.Request, *UserState) Response { return SyncResponse(nil) }