mirror of
https://github.com/status-im/consul.git
synced 2025-02-17 08:07:35 +00:00
* Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
|
|
"github.com/hashicorp/consul/internal/resource"
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
)
|
|
|
|
func NewHandler(
|
|
client pbresource.ResourceServiceClient,
|
|
registry resource.Registry,
|
|
parseToken func(req *http.Request, token *string),
|
|
logger hclog.Logger) http.Handler {
|
|
mux := http.NewServeMux()
|
|
for _, t := range registry.Types() {
|
|
// Individual Resource Endpoints.
|
|
prefix := strings.ToLower(fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind))
|
|
logger.Info("Registered resource endpoint", "endpoint", prefix)
|
|
mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken, logger}))
|
|
}
|
|
|
|
return mux
|
|
}
|
|
|
|
type writeRequest struct {
|
|
Metadata map[string]string `json:"metadata"`
|
|
Data json.RawMessage `json:"data"`
|
|
Owner *pbresource.ID `json:"owner"`
|
|
}
|
|
|
|
type resourceHandler struct {
|
|
reg resource.Registration
|
|
client pbresource.ResourceServiceClient
|
|
parseToken func(req *http.Request, token *string)
|
|
logger hclog.Logger
|
|
}
|
|
|
|
func (h *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
var token string
|
|
h.parseToken(r, &token)
|
|
ctx := metadata.AppendToOutgoingContext(r.Context(), "x-consul-token", token)
|
|
switch r.Method {
|
|
case http.MethodPut:
|
|
h.handleWrite(w, r, ctx)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ctx context.Context) {
|
|
var req writeRequest
|
|
// convert req body to writeRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte("Request body didn't follow schema."))
|
|
}
|
|
// convert data struct to proto message
|
|
data := h.reg.Proto.ProtoReflect().New().Interface()
|
|
if err := protojson.Unmarshal(req.Data, data); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte("Request body didn't follow schema."))
|
|
}
|
|
// proto message to any
|
|
anyProtoMsg, err := anypb.New(data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logger.Error("Failed to convert proto message to any type", "error", err)
|
|
return
|
|
}
|
|
|
|
tenancyInfo, resourceName, version := checkURL(r)
|
|
|
|
rsp, err := h.client.Write(ctx, &pbresource.WriteRequest{
|
|
Resource: &pbresource.Resource{
|
|
Id: &pbresource.ID{
|
|
Type: h.reg.Type,
|
|
Tenancy: tenancyInfo,
|
|
Name: resourceName,
|
|
},
|
|
Owner: req.Owner,
|
|
Version: version,
|
|
Metadata: req.Metadata,
|
|
Data: anyProtoMsg,
|
|
},
|
|
})
|
|
if err != nil {
|
|
handleResponseError(err, w, h)
|
|
return
|
|
}
|
|
|
|
output, err := jsonMarshal(rsp.Resource)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logger.Error("Failed to unmarshal GRPC resource response", "error", err)
|
|
return
|
|
}
|
|
w.Write(output)
|
|
}
|
|
|
|
func checkURL(r *http.Request) (tenancy *pbresource.Tenancy, resourceName string, version string) {
|
|
params := r.URL.Query()
|
|
tenancy = &pbresource.Tenancy{
|
|
Partition: params.Get("partition"),
|
|
PeerName: params.Get("peer_name"),
|
|
Namespace: params.Get("namespace"),
|
|
}
|
|
resourceName = path.Base(r.URL.Path)
|
|
if resourceName == "." || resourceName == "/" {
|
|
resourceName = ""
|
|
}
|
|
version = params.Get("version")
|
|
|
|
return
|
|
}
|
|
|
|
func jsonMarshal(res *pbresource.Resource) ([]byte, error) {
|
|
output, err := protojson.Marshal(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var stuff map[string]any
|
|
if err := json.Unmarshal(output, &stuff); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
delete(stuff["data"].(map[string]any), "@type")
|
|
return json.MarshalIndent(stuff, "", " ")
|
|
}
|
|
|
|
func handleResponseError(err error, w http.ResponseWriter, h *resourceHandler) {
|
|
if e, ok := status.FromError(err); ok {
|
|
switch e.Code() {
|
|
case codes.InvalidArgument:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
h.logger.Info("User has mal-formed request", "error", err)
|
|
case codes.NotFound:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
h.logger.Info("Failed to write to GRPC resource: Not found", "error", err)
|
|
case codes.PermissionDenied:
|
|
w.WriteHeader(http.StatusForbidden)
|
|
h.logger.Info("Failed to write to GRPC resource: User not authenticated", "error", err)
|
|
case codes.Aborted:
|
|
w.WriteHeader(http.StatusConflict)
|
|
h.logger.Info("Failed to write to GRPC resource: the request conflict with the current state of the target resource", "error", err)
|
|
default:
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logger.Error("Failed to write to GRPC resource", "error", err)
|
|
}
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logger.Error("Failed to write to GRPC resource: not able to parse error returned", "error", err)
|
|
}
|
|
w.Write([]byte(err.Error()))
|
|
}
|