github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/store/errors.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-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 store 21 22 import ( 23 "errors" 24 "fmt" 25 "net/url" 26 "sort" 27 "strings" 28 29 "github.com/snapcore/snapd/snap/channel" 30 "github.com/snapcore/snapd/strutil" 31 ) 32 33 var ( 34 // ErrBadQuery is returned from Find when the query has special characters in strange places. 35 ErrBadQuery = errors.New("bad query") 36 37 // ErrInvalidScope is returned from Find when an invalid scope is requested. 38 ErrInvalidScope = errors.New("invalid scope") 39 40 // ErrSnapNotFound is returned when a snap can not be found 41 ErrSnapNotFound = errors.New("snap not found") 42 43 // ErrUnauthenticated is returned when authentication is needed to complete the query 44 ErrUnauthenticated = errors.New("you need to log in first") 45 46 // ErrAuthenticationNeeds2fa is returned if the authentication needs 2factor 47 ErrAuthenticationNeeds2fa = errors.New("two factor authentication required") 48 49 // Err2faFailed is returned when 2fa failed (e.g., a bad token was given) 50 Err2faFailed = errors.New("two factor authentication failed") 51 52 // ErrInvalidCredentials is returned on login error 53 // It can also be returned when refreshing the discharge 54 // macaroon if the user has changed their password. 55 ErrInvalidCredentials = errors.New("invalid credentials") 56 57 // ErrTOSNotAccepted is returned when the user has not accepted the store's terms of service. 58 ErrTOSNotAccepted = errors.New("terms of service not accepted") 59 60 // ErrNoPaymentMethods is returned when the user has no valid payment methods associated with their account. 61 ErrNoPaymentMethods = errors.New("no payment methods") 62 63 // ErrPaymentDeclined is returned when the user's payment method was declined by the upstream payment provider. 64 ErrPaymentDeclined = errors.New("payment declined") 65 66 // ErrLocalSnap is returned when an operation that only applies to snaps that come from a store was attempted on a local snap. 67 ErrLocalSnap = errors.New("cannot perform operation on local snap") 68 69 // ErrNoUpdateAvailable is returned when an update is attempetd for a snap that has no update available. 70 ErrNoUpdateAvailable = errors.New("snap has no updates available") 71 ) 72 73 // RevisionNotAvailableError is returned when an install is attempted for a snap but the/a revision is not available (given install constraints). 74 type RevisionNotAvailableError struct { 75 Action string 76 Channel string 77 Releases []channel.Channel 78 } 79 80 func (e *RevisionNotAvailableError) Error() string { 81 return "no snap revision available as specified" 82 } 83 84 // DownloadError represents a download error 85 type DownloadError struct { 86 Code int 87 URL *url.URL 88 } 89 90 func (e *DownloadError) Error() string { 91 return fmt.Sprintf("received an unexpected http response code (%v) when trying to download %s", e.Code, e.URL) 92 } 93 94 // PasswordPolicyError is returned in a few corner cases, most notably 95 // when the password has been force-reset. 96 type PasswordPolicyError map[string]stringList 97 98 func (e PasswordPolicyError) Error() string { 99 var msg string 100 101 if reason, ok := e["reason"]; ok && len(reason) == 1 { 102 msg = reason[0] 103 if location, ok := e["location"]; ok && len(location) == 1 { 104 msg += "\nTo address this, go to: " + location[0] + "\n" 105 } 106 } else { 107 for k, vs := range e { 108 msg += fmt.Sprintf("%s: %s\n", k, strings.Join(vs, " ")) 109 } 110 } 111 112 return msg 113 } 114 115 // InvalidAuthDataError signals that the authentication data didn't pass validation. 116 type InvalidAuthDataError map[string]stringList 117 118 func (e InvalidAuthDataError) Error() string { 119 var es []string 120 for _, v := range e { 121 es = append(es, v...) 122 } 123 // XXX: confirm with server people that extra args are all 124 // full sentences (with periods and capitalization) 125 // (empirically this checks out) 126 return strings.Join(es, " ") 127 } 128 129 // SnapActionError conveys errors that were reported on otherwise overall successful snap action (install/refresh) request. 130 type SnapActionError struct { 131 // NoResults is set if there were no results in the response 132 NoResults bool 133 // Refresh errors by snap name. 134 Refresh map[string]error 135 // Install errors by snap name. 136 Install map[string]error 137 // Download errors by snap name. 138 Download map[string]error 139 // Other errors. 140 Other []error 141 } 142 143 // SingleOpError returns the single operation, snap name, and error if 144 // e represents a single error of a single operation on a single snap 145 // (i.e. if e.Other is empty, and e.Refresh, e.Install and e.Download 146 // have a single error in total). 147 // In any other case, the error returned will be nil. 148 func (e SnapActionError) SingleOpError() (op, name string, err error) { 149 if len(e.Other) > 0 { 150 return "", "", nil 151 } 152 153 nRefresh := len(e.Refresh) 154 nInstall := len(e.Install) 155 nDownload := len(e.Download) 156 if nRefresh+nInstall+nDownload != 1 { 157 return "", "", nil 158 } 159 160 var errs map[string]error 161 switch { 162 case nRefresh > 0: 163 op = "refresh" 164 errs = e.Refresh 165 case nInstall > 0: 166 op = "install" 167 errs = e.Install 168 case nDownload > 0: 169 op = "download" 170 errs = e.Download 171 } 172 for name, err = range errs { 173 return op, name, err 174 } 175 // can't happen 176 return "", "", nil 177 } 178 179 func (e SnapActionError) Error() string { 180 nRefresh := len(e.Refresh) 181 nInstall := len(e.Install) 182 nDownload := len(e.Download) 183 nOther := len(e.Other) 184 185 // single error 186 switch nRefresh + nInstall + nDownload + nOther { 187 case 0: 188 if e.NoResults { 189 // this is an atypical result 190 return "no install/refresh information results from the store" 191 } 192 case 1: 193 if nOther == 0 { 194 op, name, err := e.SingleOpError() 195 return fmt.Sprintf("cannot %s snap %q: %v", op, name, err) 196 } else { 197 return fmt.Sprintf("cannot refresh, install, or download: %v", e.Other[0]) 198 } 199 } 200 201 header := "cannot refresh, install, or download:" 202 if nOther == 0 { 203 // at least one of nDownload, nInstall, or nRefresh is > 0 204 switch { 205 case nDownload == 0 && nRefresh == 0: 206 header = "cannot install:" 207 case nDownload == 0 && nInstall == 0: 208 header = "cannot refresh:" 209 case nRefresh == 0 && nInstall == 0: 210 header = "cannot download:" 211 case nDownload == 0: 212 header = "cannot refresh or install:" 213 case nInstall == 0: 214 header = "cannot refresh or download:" 215 case nRefresh == 0: 216 header = "cannot install or download:" 217 } 218 } 219 220 // reverse the "snap->error" map to "error->snap", as the 221 // common case is that all snaps fail with the same error 222 // (e.g. "no refresh available") 223 errToSnaps := map[string][]string{} 224 errKeys := []string{} // poorman's ordered map 225 226 for _, m := range []map[string]error{e.Refresh, e.Install, e.Download} { 227 for snapName, err := range m { 228 k := err.Error() 229 v, ok := errToSnaps[k] 230 if !ok { 231 errKeys = append(errKeys, k) 232 } 233 errToSnaps[k] = append(v, snapName) 234 } 235 } 236 237 es := make([]string, 1, 1+len(errToSnaps)+nOther) 238 es[0] = header 239 for _, k := range errKeys { 240 sort.Strings(errToSnaps[k]) 241 es = append(es, fmt.Sprintf("%s: %s", k, strutil.Quoted(errToSnaps[k]))) 242 } 243 244 for _, e := range e.Other { 245 es = append(es, e.Error()) 246 } 247 248 if len(es) == 2 { 249 // header + 1 reason 250 return strings.Join(es, " ") 251 } 252 253 return strings.Join(es, "\n") 254 } 255 256 // Authorization soft-expiry errors that get handled automatically. 257 var ( 258 errUserAuthorizationNeedsRefresh = errors.New("soft-expired user authorization needs refresh") 259 errDeviceAuthorizationNeedsRefresh = errors.New("soft-expired device authorization needs refresh") 260 ) 261 262 func translateSnapActionError(action, snapChannel, code, message string, releases []snapRelease) error { 263 switch code { 264 case "revision-not-found": 265 e := &RevisionNotAvailableError{ 266 Action: action, 267 Channel: snapChannel, 268 } 269 if len(releases) != 0 { 270 parsedReleases := make([]channel.Channel, len(releases)) 271 for i := 0; i < len(releases); i++ { 272 var err error 273 parsedReleases[i], err = channel.Parse(releases[i].Channel, releases[i].Architecture) 274 if err != nil { 275 // shouldn't happen, return error without Releases 276 return e 277 } 278 } 279 e.Releases = parsedReleases 280 } 281 return e 282 case "id-not-found", "name-not-found": 283 return ErrSnapNotFound 284 case "user-authorization-needs-refresh": 285 return errUserAuthorizationNeedsRefresh 286 case "device-authorization-needs-refresh": 287 return errDeviceAuthorizationNeedsRefresh 288 default: 289 return fmt.Errorf("%v", message) 290 } 291 }