-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
server: add /api/v2/ tree with auth/pagination, port some status endp…
…oints This change adds the skeleton for a new API tree that lives in `/api/v2/` in the http listener, and currently reimplements a few endpoints that are also implemented in `/_status/`. The new v2 API tree avoids the need to use GRPC Gateway, as well as cookie-based authentication which is less intuitive and idiomatic for REST APIs. Instead, for authentication, it uses a new session header that needs to be set on every request. As many RPC fan-out APIs use statusServer.iterateNodes, this change implements a pagination-aware method, paginatedIterateNodes, that works on a sorted set of node IDs and arranges results in such a way to be able to return the next `limit` results of an arbitary slice after the `next` cursor passed in. An example of how this works in practice is the new `/api/v2/sessions/` endpoint. A dependency on gorilla/mux is added to be able to pattern-match arguments in the URL. This was already an indirect dependency; now it's a direct dependency of cockroach. TODO that are likely to fall over into future PRs: - API Documentation, need to explore using swagger here. - Porting over remaining /_admin/ and /_status/ APIs, incl. SQL based ones Part of #55947. Release note (api change): Adds a new API tree, in /api/v2/*, currently undocumented, that avoids the use of and cookie-based authentication in favour of sessions in headers, and support for pagination.
- Loading branch information
Showing
16 changed files
with
1,361 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
// Copyright 2021 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
package server | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"strconv" | ||
|
||
"github.com/cockroachdb/cockroach/pkg/server/serverpb" | ||
"github.com/gorilla/mux" | ||
) | ||
|
||
const ( | ||
apiV2Path = "/api/v2/" | ||
apiV2AuthHeader = "X-Cockroach-API-Session" | ||
) | ||
|
||
func writeJsonResponse(w http.ResponseWriter, code int, payload interface{}) { | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(code) | ||
|
||
res, err := json.Marshal(payload) | ||
if err != nil { | ||
panic(err) | ||
} | ||
if _, err := w.Write(res); err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
// apiV2Server implements endpoints under apiV2Path. | ||
type apiV2Server struct { | ||
admin *adminServer | ||
status *statusServer | ||
mux *mux.Router | ||
} | ||
|
||
func newApiServer(ctx context.Context, s *Server) *apiV2Server { | ||
authServer := newAuthenticationV2Server(ctx, s, apiV2Path) | ||
innerMux := mux.NewRouter() | ||
|
||
authMux := newAuthenticationV2Mux(authServer, innerMux) | ||
outerMux := mux.NewRouter() | ||
a := &apiV2Server{ | ||
admin: s.admin, | ||
status: s.status, | ||
mux: outerMux, | ||
} | ||
a.registerRoutes(innerMux, authMux) | ||
a.mux.Handle(apiV2Path + "login/", authServer) | ||
a.mux.Handle(apiV2Path + "logout/", authServer) | ||
return a | ||
} | ||
|
||
func (a *apiV2Server) registerRoutes(innerMux *mux.Router, authMux http.Handler) { | ||
routeDefinitions := []struct{ | ||
endpoint string | ||
handler http.HandlerFunc | ||
requiresAuth bool | ||
requiresAdmin bool | ||
}{ | ||
{"sessions/", a.listSessions, true /* requiresAuth */ , true /* requiresAdmin */}, | ||
{"hotranges/", a.hotRanges, true /* requiresAuth */ , true /* requiresAdmin */}, | ||
{"ranges/{range_id}/", a.rangeHandler, true /* requiresAuth */ , true /* requiresAdmin */}, | ||
{"nodes/", a.nodes, true /* requiresAuth */ , false /* requiresAdmin */}, | ||
} | ||
|
||
// For all routes requiring authentication, have the outer mux (a.mux) | ||
// send requests through to the authMux, and also register the relevant route | ||
// in innerMux. Routes not requiring login can directly be handled in a.mux. | ||
for _, route := range routeDefinitions { | ||
if route.requiresAuth { | ||
a.mux.Handle(apiV2Path + route.endpoint, authMux) | ||
handler := http.Handler(route.handler) | ||
if route.requiresAdmin { | ||
handler = &requireAdminWrapper{ | ||
a: a.admin.adminPrivilegeChecker, | ||
inner: route.handler, | ||
} | ||
} | ||
innerMux.Handle(apiV2Path + route.endpoint, handler) | ||
} else { | ||
a.mux.HandleFunc(apiV2Path + route.endpoint, route.handler) | ||
} | ||
} | ||
} | ||
|
||
type listSessionsResponse struct { | ||
serverpb.ListSessionsResponse | ||
|
||
Next string `json:"next"` | ||
} | ||
|
||
func (a *apiV2Server) listSessions(w http.ResponseWriter, r *http.Request) { | ||
limit, start := getRPCPaginationValues(r) | ||
req := &serverpb.ListSessionsRequest{Username: r.Context().Value(webSessionUserKey{}).(string)} | ||
response := &listSessionsResponse{} | ||
|
||
responseProto, pagState, err := a.status.listSessionsHelper(r.Context(), req, limit, start) | ||
var nextBytes []byte | ||
if nextBytes, err = pagState.MarshalText(); err != nil { | ||
err := serverpb.ListSessionsError{Message: err.Error()} | ||
response.Errors = append(response.Errors, err) | ||
} else { | ||
response.Next = string(nextBytes) | ||
} | ||
response.ListSessionsResponse = *responseProto | ||
writeJsonResponse(w, http.StatusOK, response) | ||
} | ||
|
||
type rangeResponse struct { | ||
serverpb.RangeResponse | ||
|
||
Next string `json:"next"` | ||
} | ||
|
||
func (a *apiV2Server) rangeHandler(w http.ResponseWriter, r *http.Request) { | ||
limit, start := getRPCPaginationValues(r) | ||
var err error | ||
var rangeID int64 | ||
vars := mux.Vars(r) | ||
if rangeID, err = strconv.ParseInt(vars["range_id"], 10, 64); err != nil { | ||
http.Error(w, "invalid range id", http.StatusBadRequest) | ||
return | ||
} | ||
|
||
req := &serverpb.RangeRequest{RangeId: rangeID} | ||
response := &rangeResponse{} | ||
responseProto, next, err := a.status.rangeHelper(r.Context(), req, limit, start) | ||
if err != nil { | ||
apiV2InternalError(r.Context(), err, w) | ||
return | ||
} | ||
response.RangeResponse = *responseProto | ||
if nextBytes, err := next.MarshalText(); err == nil { | ||
response.Next = string(nextBytes) | ||
} | ||
writeJsonResponse(w, http.StatusOK, response) | ||
} | ||
|
||
type hotRangesResponse struct { | ||
serverpb.HotRangesResponse | ||
|
||
Next string `json:"next"` | ||
} | ||
|
||
func (a *apiV2Server) hotRanges(w http.ResponseWriter, r *http.Request) { | ||
limit, start := getRPCPaginationValues(r) | ||
req := &serverpb.HotRangesRequest{NodeID: r.URL.Query().Get("node_id")} | ||
response := &hotRangesResponse{} | ||
|
||
responseProto, next, err := a.status.hotRangesHelper(r.Context(), req, limit, start) | ||
if err != nil { | ||
apiV2InternalError(r.Context(), err, w) | ||
return | ||
} | ||
response.HotRangesResponse = *responseProto | ||
if nextBytes, err := next.MarshalText(); err == nil { | ||
response.Next = string(nextBytes) | ||
} | ||
writeJsonResponse(w, http.StatusOK, response) | ||
} | ||
|
||
type nodesResponse struct { | ||
serverpb.NodesResponse | ||
|
||
Next int `json:"next"` | ||
} | ||
|
||
func (a *apiV2Server) nodes(w http.ResponseWriter, r *http.Request) { | ||
limit, offset := getSimplePaginationValues(r) | ||
req := &serverpb.NodesRequest{} | ||
response := &nodesResponse{} | ||
|
||
responseProto, next, err := a.status.nodesHelper(r.Context(), req, limit, offset) | ||
if err != nil { | ||
apiV2InternalError(r.Context(), err, w) | ||
return | ||
} | ||
response.NodesResponse = *responseProto | ||
response.Next = next | ||
writeJsonResponse(w, http.StatusOK, response) | ||
} | ||
|
||
func (a *apiV2Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
a.mux.ServeHTTP(w, r) | ||
} | ||
|
Oops, something went wrong.