summaryrefslogtreecommitdiff
path: root/pkg/api/handlers/utils/handler.go
blob: 517dccad04266502ad46f92f8410dcfeb4e216a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package utils

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/blang/semver"
	"github.com/gorilla/mux"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

type (
	// VersionTree determines which API endpoint tree for version
	VersionTree int
	// VersionLevel determines which API level, current or something from the past
	VersionLevel int
)

const (
	// LibpodTree supports Libpod endpoints
	LibpodTree = VersionTree(iota)
	// CompatTree supports Libpod endpoints
	CompatTree

	// CurrentAPIVersion announces what is the current API level
	CurrentAPIVersion = VersionLevel(iota)
	// MinimalAPIVersion announces what is the oldest API level supported
	MinimalAPIVersion
)

var (
	// See https://docs.docker.com/engine/api/v1.40/
	// libpod compat handlers are expected to honor docker API versions

	// APIVersion provides the current and minimal API versions for compat and libpod endpoint trees
	// Note: GET|HEAD /_ping is never versioned and provides the API-Version and Libpod-API-Version headers to allow
	//       clients to shop for the Version they wish to support
	APIVersion = map[VersionTree]map[VersionLevel]semver.Version{
		LibpodTree: {
			CurrentAPIVersion: semver.MustParse("2.0.0"),
			MinimalAPIVersion: semver.MustParse("2.0.0"),
		},
		CompatTree: {
			CurrentAPIVersion: semver.MustParse("1.40.0"),
			MinimalAPIVersion: semver.MustParse("1.24.0"),
		},
	}

	// ErrVersionNotGiven returned when version not given by client
	ErrVersionNotGiven = errors.New("version not given in URL path")
	// ErrVersionNotSupported returned when given version is too old
	ErrVersionNotSupported = errors.New("given version is not supported")
)

// IsLibpodRequest returns true if the request related to a libpod endpoint
// (e.g., /v2/libpod/...).
func IsLibpodRequest(r *http.Request) bool {
	split := strings.Split(r.URL.String(), "/")
	return len(split) >= 3 && split[2] == "libpod"
}

// SupportedVersion validates that the version provided by client is included in the given condition
// https://github.com/blang/semver#ranges provides the details for writing conditions
// If a version is not given in URL path, ErrVersionNotGiven is returned
func SupportedVersion(r *http.Request, condition string) (semver.Version, error) {
	version := semver.Version{}
	val, ok := mux.Vars(r)["version"]
	if !ok {
		return version, ErrVersionNotGiven
	}
	safeVal, err := url.PathUnescape(val)
	if err != nil {
		return version, errors.Wrapf(err, "unable to unescape given API version: %q", val)
	}
	version, err = semver.ParseTolerant(safeVal)
	if err != nil {
		return version, errors.Wrapf(err, "unable to parse given API version: %q from %q", safeVal, val)
	}

	inRange, err := semver.ParseRange(condition)
	if err != nil {
		return version, err
	}

	if inRange(version) {
		return version, nil
	}
	return version, ErrVersionNotSupported
}

// SupportedVersionWithDefaults validates that the version provided by client valid is supported by server
// minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL
func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) {
	tree := CompatTree
	if IsLibpodRequest(r) {
		tree = LibpodTree
	}

	return SupportedVersion(r,
		fmt.Sprintf(">=%s <=%s", APIVersion[tree][MinimalAPIVersion].String(),
			APIVersion[tree][CurrentAPIVersion].String()))
}

// WriteResponse encodes the given value as JSON or string and renders it for http client
func WriteResponse(w http.ResponseWriter, code int, value interface{}) {
	// RFC2616 explicitly states that the following status codes "MUST NOT
	// include a message-body":
	switch code {
	case http.StatusNoContent, http.StatusNotModified: // 204, 304
		w.WriteHeader(code)
		return
	}

	switch v := value.(type) {
	case string:
		w.Header().Set("Content-Type", "text/plain; charset=us-ascii")
		w.WriteHeader(code)

		if _, err := fmt.Fprintln(w, v); err != nil {
			logrus.Errorf("unable to send string response: %q", err)
		}
	case *os.File:
		w.Header().Set("Content-Type", "application/octet; charset=us-ascii")
		w.WriteHeader(code)

		if _, err := io.Copy(w, v); err != nil {
			logrus.Errorf("unable to copy to response: %q", err)
		}
	case io.Reader:
		w.Header().Set("Content-Type", "application/x-tar")
		w.WriteHeader(code)

		if _, err := io.Copy(w, v); err != nil {
			logrus.Errorf("unable to copy to response: %q", err)
		}
	default:
		WriteJSON(w, code, value)
	}
}

func WriteJSON(w http.ResponseWriter, code int, value interface{}) {
	// FIXME: we don't need to write the header in all/some circumstances.
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)

	coder := json.NewEncoder(w)
	coder.SetEscapeHTML(true)
	if err := coder.Encode(value); err != nil {
		logrus.Errorf("unable to write json: %q", err)
	}
}

func FilterMapToString(filters map[string][]string) (string, error) {
	f, err := json.Marshal(filters)
	if err != nil {
		return "", err
	}
	return string(f), nil
}

func getVar(r *http.Request, k string) string {
	val := mux.Vars(r)[k]
	safeVal, err := url.PathUnescape(val)
	if err != nil {
		logrus.Error(errors.Wrapf(err, "failed to unescape mux key %s, value %s", k, val))
		return val
	}
	return safeVal
}

// GetName extracts the name from the mux
func GetName(r *http.Request) string {
	return getVar(r, "name")
}