mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
Internal Server Error response was returned when only one of many snaps is not found. The problem was passing all snap names (including succesful ones) to errToResponse instead of the unknown snap only. In the following example only "spamandeggs" is not found, 404 status code is expected, 500 is returned instead: curl -sS --unix-socket /run/snapd.socket http://localhost/v2/snaps -X POST \ -d '{"action": "install", "snaps": ["spamandeggs", "hello"]}' -H "Content-Type: application/json" \ | jq > { > "type": "error", > "status-code": 500, > "status": "Internal Server Error", > "result": { > "message": "store.SnapNotFound with 2 snaps" > } > } Fixes: https://bugs.launchpad.net/snapd/+bug/2024858 Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
374 lines
10 KiB
Go
374 lines
10 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2015-2021 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 daemon
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
|
|
"github.com/snapcore/snapd/arch"
|
|
"github.com/snapcore/snapd/client"
|
|
"github.com/snapcore/snapd/overlord/servicestate"
|
|
"github.com/snapcore/snapd/overlord/snapstate"
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/store"
|
|
)
|
|
|
|
// apiError represents an error meant for returning to the client.
|
|
// It can serialize itself to our standard JSON response format.
|
|
type apiError struct {
|
|
// Status is the error HTTP status code.
|
|
Status int
|
|
Message string
|
|
// Kind is the error kind. See client/errors.go
|
|
Kind client.ErrorKind
|
|
Value errorValue
|
|
}
|
|
|
|
func (ae *apiError) Error() string {
|
|
kindOrStatus := "api"
|
|
if ae.Kind != "" {
|
|
kindOrStatus = fmt.Sprintf("api: %s", ae.Kind)
|
|
} else if ae.Status != 400 {
|
|
kindOrStatus = fmt.Sprintf("api %d", ae.Status)
|
|
}
|
|
return fmt.Sprintf("%s (%s)", ae.Message, kindOrStatus)
|
|
}
|
|
|
|
func (ae *apiError) JSON() *respJSON {
|
|
return &respJSON{
|
|
Status: ae.Status,
|
|
Type: ResponseTypeError,
|
|
Result: &errorResult{
|
|
Message: ae.Message,
|
|
Kind: ae.Kind,
|
|
Value: ae.Value,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (ae *apiError) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ae.JSON().ServeHTTP(w, r)
|
|
}
|
|
|
|
// check it implements StructuredResponse
|
|
var _ StructuredResponse = (*apiError)(nil)
|
|
|
|
type errorValue interface{}
|
|
|
|
type errorResult struct {
|
|
Message string `json:"message"` // note no omitempty
|
|
// Kind is the error kind. See client/errors.go
|
|
Kind client.ErrorKind `json:"kind,omitempty"`
|
|
Value errorValue `json:"value,omitempty"`
|
|
}
|
|
|
|
// errorResponder is a callable that produces an error Response.
|
|
// e.g., InternalError("something broke: %v", err), etc.
|
|
type errorResponder func(string, ...interface{}) *apiError
|
|
|
|
// makeErrorResponder builds an errorResponder from the given error status.
|
|
func makeErrorResponder(status int) errorResponder {
|
|
return func(format string, v ...interface{}) *apiError {
|
|
var msg string
|
|
if len(v) == 0 {
|
|
msg = format
|
|
} else {
|
|
msg = fmt.Sprintf(format, v...)
|
|
}
|
|
var kind client.ErrorKind
|
|
if status == 401 || status == 403 {
|
|
kind = client.ErrorKindLoginRequired
|
|
}
|
|
return &apiError{
|
|
Status: status,
|
|
Message: msg,
|
|
Kind: kind,
|
|
}
|
|
}
|
|
}
|
|
|
|
// standard error responses
|
|
var (
|
|
Unauthorized = makeErrorResponder(401)
|
|
NotFound = makeErrorResponder(404)
|
|
BadRequest = makeErrorResponder(400)
|
|
MethodNotAllowed = makeErrorResponder(405)
|
|
InternalError = makeErrorResponder(500)
|
|
NotImplemented = makeErrorResponder(501)
|
|
Forbidden = makeErrorResponder(403)
|
|
Conflict = makeErrorResponder(409)
|
|
)
|
|
|
|
// BadQuery is an error responder used when a bad query was
|
|
// provided to the store.
|
|
func BadQuery() *apiError {
|
|
return &apiError{
|
|
Status: 400,
|
|
Message: "bad query",
|
|
Kind: client.ErrorKindBadQuery,
|
|
}
|
|
}
|
|
|
|
// SnapNotFound is an error responder used when an operation is
|
|
// requested on a snap that doesn't exist.
|
|
func SnapNotFound(snapName string, err error) *apiError {
|
|
return &apiError{
|
|
Status: 404,
|
|
Message: err.Error(),
|
|
Kind: client.ErrorKindSnapNotFound,
|
|
Value: snapName,
|
|
}
|
|
}
|
|
|
|
// SnapRevisionNotAvailable is an error responder used when an
|
|
// operation is requested for which no revivision can be found
|
|
// in the given context (e.g. request an install from a stable
|
|
// channel when this channel is empty).
|
|
func SnapRevisionNotAvailable(snapName string, rnaErr *store.RevisionNotAvailableError) *apiError {
|
|
var value interface{} = snapName
|
|
kind := client.ErrorKindSnapRevisionNotAvailable
|
|
msg := rnaErr.Error()
|
|
if len(rnaErr.Releases) != 0 && rnaErr.Channel != "" {
|
|
thisArch := arch.DpkgArchitecture()
|
|
values := map[string]interface{}{
|
|
"snap-name": snapName,
|
|
"action": rnaErr.Action,
|
|
"channel": rnaErr.Channel,
|
|
"architecture": thisArch,
|
|
}
|
|
archOK := false
|
|
releases := make([]map[string]interface{}, 0, len(rnaErr.Releases))
|
|
for _, c := range rnaErr.Releases {
|
|
if c.Architecture == thisArch {
|
|
archOK = true
|
|
}
|
|
releases = append(releases, map[string]interface{}{
|
|
"architecture": c.Architecture,
|
|
"channel": c.Name,
|
|
})
|
|
}
|
|
// we return all available releases (arch x channel)
|
|
// as reported in the store error, but we hint with
|
|
// the error kind whether there was anything at all
|
|
// available for this architecture
|
|
if archOK {
|
|
kind = client.ErrorKindSnapChannelNotAvailable
|
|
msg = "no snap revision on specified channel"
|
|
} else {
|
|
kind = client.ErrorKindSnapArchitectureNotAvailable
|
|
msg = "no snap revision on specified architecture"
|
|
}
|
|
values["releases"] = releases
|
|
value = values
|
|
}
|
|
return &apiError{
|
|
Status: 404,
|
|
Message: msg,
|
|
Kind: kind,
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
// SnapChangeConflict is an error responder used when an operation would
|
|
// conflict with another ongoing change.
|
|
func SnapChangeConflict(cce *snapstate.ChangeConflictError) *apiError {
|
|
value := map[string]interface{}{}
|
|
if cce.Snap != "" {
|
|
value["snap-name"] = cce.Snap
|
|
}
|
|
if cce.ChangeKind != "" {
|
|
value["change-kind"] = cce.ChangeKind
|
|
}
|
|
|
|
return &apiError{
|
|
Status: 409,
|
|
Message: cce.Error(),
|
|
Kind: client.ErrorKindSnapChangeConflict,
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
// QuotaChangeConflict is an error responder used when an operation would
|
|
// conflict with another ongoing change.
|
|
func QuotaChangeConflict(qce *servicestate.QuotaChangeConflictError) *apiError {
|
|
value := map[string]interface{}{}
|
|
if qce.Quota != "" {
|
|
value["quota-name"] = qce.Quota
|
|
}
|
|
if qce.ChangeKind != "" {
|
|
value["change-kind"] = qce.ChangeKind
|
|
}
|
|
|
|
return &apiError{
|
|
Status: 409,
|
|
Message: qce.Error(),
|
|
Kind: client.ErrorKindQuotaChangeConflict,
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
// InsufficientSpace is an error responder used when an operation cannot
|
|
// be performed due to low disk space.
|
|
func InsufficientSpace(dserr *snapstate.InsufficientSpaceError) *apiError {
|
|
value := map[string]interface{}{}
|
|
if len(dserr.Snaps) > 0 {
|
|
value["snap-names"] = dserr.Snaps
|
|
}
|
|
if dserr.ChangeKind != "" {
|
|
value["change-kind"] = dserr.ChangeKind
|
|
}
|
|
return &apiError{
|
|
Status: 507,
|
|
Message: dserr.Error(),
|
|
Kind: client.ErrorKindInsufficientDiskSpace,
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
// AppNotFound is an error responder used when an operation is
|
|
// requested on a app that doesn't exist.
|
|
func AppNotFound(format string, v ...interface{}) *apiError {
|
|
return &apiError{
|
|
Status: 404,
|
|
Message: fmt.Sprintf(format, v...),
|
|
Kind: client.ErrorKindAppNotFound,
|
|
}
|
|
}
|
|
|
|
// AuthCancelled is an error responder used when a user cancelled
|
|
// the auth process.
|
|
func AuthCancelled(format string, v ...interface{}) *apiError {
|
|
return &apiError{
|
|
Status: 403,
|
|
Message: fmt.Sprintf(format, v...),
|
|
Kind: client.ErrorKindAuthCancelled,
|
|
}
|
|
}
|
|
|
|
// InterfacesUnchanged is an error responder used when an operation
|
|
// that would normally change interfaces finds it has nothing to do
|
|
func InterfacesUnchanged(format string, v ...interface{}) *apiError {
|
|
return &apiError{
|
|
Status: 400,
|
|
Message: fmt.Sprintf(format, v...),
|
|
Kind: client.ErrorKindInterfacesUnchanged,
|
|
}
|
|
}
|
|
|
|
func errToResponse(err error, snaps []string, fallback errorResponder, format string, v ...interface{}) *apiError {
|
|
var kind client.ErrorKind
|
|
var snapName string
|
|
|
|
switch err {
|
|
case store.ErrSnapNotFound:
|
|
switch len(snaps) {
|
|
case 1:
|
|
return SnapNotFound(snaps[0], err)
|
|
// store.ErrSnapNotFound should only be returned for individual
|
|
// snap queries; in all other cases something's wrong
|
|
case 0:
|
|
return InternalError("store.SnapNotFound with no snap given")
|
|
default:
|
|
return InternalError("store.SnapNotFound with %d snaps", len(snaps))
|
|
}
|
|
case store.ErrNoUpdateAvailable:
|
|
kind = client.ErrorKindSnapNoUpdateAvailable
|
|
case store.ErrLocalSnap:
|
|
kind = client.ErrorKindSnapLocal
|
|
default:
|
|
handled := true
|
|
switch err := err.(type) {
|
|
case *store.RevisionNotAvailableError:
|
|
// store.ErrRevisionNotAvailable should only be returned for
|
|
// individual snap queries; in all other cases something's wrong
|
|
switch len(snaps) {
|
|
case 1:
|
|
return SnapRevisionNotAvailable(snaps[0], err)
|
|
case 0:
|
|
return InternalError("store.RevisionNotAvailable with no snap given")
|
|
default:
|
|
return InternalError("store.RevisionNotAvailable with %d snaps", len(snaps))
|
|
}
|
|
case *snap.AlreadyInstalledError:
|
|
kind = client.ErrorKindSnapAlreadyInstalled
|
|
snapName = err.Snap
|
|
case *snap.NotInstalledError:
|
|
kind = client.ErrorKindSnapNotInstalled
|
|
snapName = err.Snap
|
|
case *servicestate.QuotaChangeConflictError:
|
|
return QuotaChangeConflict(err)
|
|
case *snapstate.SnapNeedsDevModeError:
|
|
kind = client.ErrorKindSnapNeedsDevMode
|
|
snapName = err.Snap
|
|
case *snapstate.SnapNeedsClassicError:
|
|
kind = client.ErrorKindSnapNeedsClassic
|
|
snapName = err.Snap
|
|
case *snapstate.SnapNeedsClassicSystemError:
|
|
kind = client.ErrorKindSnapNeedsClassicSystem
|
|
snapName = err.Snap
|
|
case *snapstate.SnapNotClassicError:
|
|
kind = client.ErrorKindSnapNotClassic
|
|
snapName = err.Snap
|
|
case *snapstate.InsufficientSpaceError:
|
|
return InsufficientSpace(err)
|
|
case net.Error:
|
|
if err.Timeout() {
|
|
kind = client.ErrorKindNetworkTimeout
|
|
} else {
|
|
handled = false
|
|
}
|
|
case *store.SnapActionError:
|
|
// we only handle a few specific cases
|
|
_, name, e := err.SingleOpError()
|
|
if e != nil {
|
|
// 👉😎👉
|
|
return errToResponse(e, []string{name}, fallback, format)
|
|
}
|
|
handled = false
|
|
default:
|
|
// support wrapped errors
|
|
switch {
|
|
case errors.Is(err, &snapstate.ChangeConflictError{}):
|
|
var conflErr *snapstate.ChangeConflictError
|
|
if errors.As(err, &conflErr) {
|
|
return SnapChangeConflict(conflErr)
|
|
}
|
|
}
|
|
|
|
handled = false
|
|
}
|
|
|
|
if !handled {
|
|
v = append(v, err)
|
|
return fallback(format, v...)
|
|
}
|
|
}
|
|
|
|
return &apiError{
|
|
Status: 400,
|
|
Message: err.Error(),
|
|
Kind: kind,
|
|
Value: snapName,
|
|
}
|
|
}
|