mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
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
|
|
_, _, e := err.SingleOpError()
|
|
if e != nil {
|
|
// 👉😎👉
|
|
return errToResponse(e, snaps, 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,
|
|
}
|
|
}
|