github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/api_apps.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2020 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 daemon 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "net/http" 26 "sort" 27 "strconv" 28 "strings" 29 30 "github.com/snapcore/snapd/client/clientutil" 31 "github.com/snapcore/snapd/overlord/auth" 32 "github.com/snapcore/snapd/overlord/servicestate" 33 "github.com/snapcore/snapd/overlord/state" 34 "github.com/snapcore/snapd/progress" 35 "github.com/snapcore/snapd/snap" 36 "github.com/snapcore/snapd/strutil" 37 "github.com/snapcore/snapd/systemd" 38 ) 39 40 var ( 41 appsCmd = &Command{ 42 Path: "/v2/apps", 43 GET: getAppsInfo, 44 POST: postApps, 45 ReadAccess: openAccess{}, 46 WriteAccess: authenticatedAccess{}, 47 } 48 49 logsCmd = &Command{ 50 Path: "/v2/logs", 51 GET: getLogs, 52 ReadAccess: authenticatedAccess{Polkit: polkitActionManage}, 53 } 54 ) 55 56 func getAppsInfo(c *Command, r *http.Request, user *auth.UserState) Response { 57 query := r.URL.Query() 58 59 opts := appInfoOptions{} 60 switch sel := query.Get("select"); sel { 61 case "": 62 // nothing to do 63 case "service": 64 opts.service = true 65 default: 66 return BadRequest("invalid select parameter: %q", sel) 67 } 68 69 appInfos, rspe := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts) 70 if rspe != nil { 71 return rspe 72 } 73 74 sd := servicestate.NewStatusDecorator(progress.Null) 75 76 clientAppInfos, err := clientutil.ClientAppInfosFromSnapAppInfos(appInfos, sd) 77 if err != nil { 78 return InternalError("%v", err) 79 } 80 81 return SyncResponse(clientAppInfos) 82 } 83 84 type appInfoOptions struct { 85 service bool 86 } 87 88 func (opts appInfoOptions) String() string { 89 if opts.service { 90 return "service" 91 } 92 93 return "app" 94 } 95 96 // appInfosFor returns a sorted list apps described by names. 97 // 98 // * If names is empty, returns all apps of the wanted kinds (which 99 // could be an empty list). 100 // * An element of names can be a snap name, in which case all apps 101 // from the snap of the wanted kind are included in the result (and 102 // it's an error if the snap has no apps of the wanted kind). 103 // * An element of names can instead be snap.app, in which case that app is 104 // included in the result (and it's an error if the snap and app don't 105 // both exist, or if the app is not a wanted kind) 106 // On error an appropriate *apiError is returned; a nil *apiError means 107 // no error. 108 // 109 // It's a programming error to call this with wanted having neither 110 // services nor commands set. 111 func appInfosFor(st *state.State, names []string, opts appInfoOptions) ([]*snap.AppInfo, *apiError) { 112 snapNames := make(map[string]bool) 113 requested := make(map[string]bool) 114 for _, name := range names { 115 requested[name] = true 116 name, _ = splitAppName(name) 117 snapNames[name] = true 118 } 119 120 snaps, err := allLocalSnapInfos(st, false, snapNames) 121 if err != nil { 122 return nil, InternalError("cannot list local snaps! %v", err) 123 } 124 125 found := make(map[string]bool) 126 appInfos := make([]*snap.AppInfo, 0, len(requested)) 127 for _, snp := range snaps { 128 snapName := snp.info.InstanceName() 129 apps := make([]*snap.AppInfo, 0, len(snp.info.Apps)) 130 for _, app := range snp.info.Apps { 131 if !opts.service || app.IsService() { 132 apps = append(apps, app) 133 } 134 } 135 136 if len(apps) == 0 && requested[snapName] { 137 return nil, AppNotFound("snap %q has no %ss", snapName, opts) 138 } 139 140 includeAll := len(requested) == 0 || requested[snapName] 141 if includeAll { 142 // want all services in a snap 143 found[snapName] = true 144 } 145 146 for _, app := range apps { 147 appName := snapName + "." + app.Name 148 if includeAll || requested[appName] { 149 appInfos = append(appInfos, app) 150 found[appName] = true 151 } 152 } 153 } 154 155 for k := range requested { 156 if !found[k] { 157 if snapNames[k] { 158 return nil, SnapNotFound(k, fmt.Errorf("snap %q not found", k)) 159 } else { 160 snap, app := splitAppName(k) 161 return nil, AppNotFound("snap %q has no %s %q", snap, opts, app) 162 } 163 } 164 } 165 166 sort.Sort(snap.AppInfoBySnapApp(appInfos)) 167 168 return appInfos, nil 169 } 170 171 // this differs from snap.SplitSnapApp in the handling of the 172 // snap-only case: 173 // snap.SplitSnapApp("foo") is ("foo", "foo"), 174 // splitAppName("foo") is ("foo", ""). 175 func splitAppName(s string) (snap, app string) { 176 if idx := strings.IndexByte(s, '.'); idx > -1 { 177 return s[:idx], s[idx+1:] 178 } 179 180 return s, "" 181 } 182 183 func getLogs(c *Command, r *http.Request, user *auth.UserState) Response { 184 query := r.URL.Query() 185 n := 10 186 if s := query.Get("n"); s != "" { 187 m, err := strconv.ParseInt(s, 0, 32) 188 if err != nil { 189 return BadRequest(`invalid value for n: %q: %v`, s, err) 190 } 191 n = int(m) 192 } 193 follow := false 194 if s := query.Get("follow"); s != "" { 195 f, err := strconv.ParseBool(s) 196 if err != nil { 197 return BadRequest(`invalid value for follow: %q: %v`, s, err) 198 } 199 follow = f 200 } 201 202 // only services have logs for now 203 opts := appInfoOptions{service: true} 204 appInfos, rspe := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts) 205 if rspe != nil { 206 return rspe 207 } 208 if len(appInfos) == 0 { 209 return AppNotFound("no matching services") 210 } 211 212 serviceNames := make([]string, len(appInfos)) 213 for i, appInfo := range appInfos { 214 serviceNames[i] = appInfo.ServiceName() 215 } 216 217 sysd := systemd.New(systemd.SystemMode, progress.Null) 218 reader, err := sysd.LogReader(serviceNames, n, follow) 219 if err != nil { 220 return InternalError("cannot get logs: %v", err) 221 } 222 223 return &journalLineReaderSeqResponse{ 224 ReadCloser: reader, 225 follow: follow, 226 } 227 } 228 229 var servicestateControl = servicestate.Control 230 231 func postApps(c *Command, r *http.Request, user *auth.UserState) Response { 232 var inst servicestate.Instruction 233 decoder := json.NewDecoder(r.Body) 234 if err := decoder.Decode(&inst); err != nil { 235 return BadRequest("cannot decode request body into service operation: %v", err) 236 } 237 // XXX: decoder.More() 238 if len(inst.Names) == 0 { 239 // on POST, don't allow empty to mean all 240 return BadRequest("cannot perform operation on services without a list of services to operate on") 241 } 242 243 st := c.d.overlord.State() 244 appInfos, rspe := appInfosFor(st, inst.Names, appInfoOptions{service: true}) 245 if rspe != nil { 246 return rspe 247 } 248 if len(appInfos) == 0 { 249 // can't happen: appInfosFor with a non-empty list of services 250 // shouldn't ever return an empty appInfos with no error response 251 return InternalError("no services found") 252 } 253 254 // do not pass flags - only create service-control tasks, do not create 255 // exec-command tasks for old snapd. These are not needed since we are 256 // handling momentary snap service commands. 257 st.Lock() 258 defer st.Unlock() 259 tss, err := servicestateControl(st, appInfos, &inst, nil, nil) 260 if err != nil { 261 // TODO: use errToResponse here too and introduce a proper error kind ? 262 if _, ok := err.(servicestate.ServiceActionConflictError); ok { 263 return Conflict(err.Error()) 264 } 265 return BadRequest(err.Error()) 266 } 267 // names received in the request can be snap or snap.app, we need to 268 // extract the actual snap names before associating them with a change 269 chg := newChange(st, "service-control", "Running service command", tss, namesToSnapNames(&inst)) 270 st.EnsureBefore(0) 271 return AsyncResponse(nil, chg.ID()) 272 } 273 274 func namesToSnapNames(inst *servicestate.Instruction) []string { 275 seen := make(map[string]struct{}, len(inst.Names)) 276 for _, snapOrSnapDotApp := range inst.Names { 277 snapName, _ := snap.SplitSnapApp(snapOrSnapDotApp) 278 seen[snapName] = struct{}{} 279 } 280 names := make([]string, 0, len(seen)) 281 for k := range seen { 282 names = append(names, k) 283 } 284 // keep stable ordering 285 sort.Strings(names) 286 return names 287 }