Ensure controller is initialized before raft. Fixes #2279

This commit is contained in:
Paul Lorenz
2024-08-01 11:29:28 -04:00
parent beebc1b53e
commit b8f944326f
20 changed files with 312 additions and 233 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ func NewRequestContext(rw http.ResponseWriter, r *http.Request) api.RequestConte
Request: r,
}
requestContext.Responder = api.NewResponder(requestContext, fabricResponseMapper{})
requestContext.Responder = api.NewResponder(requestContext, FabricResponseMapper{})
return requestContext
}
-181
View File
@@ -1,181 +0,0 @@
/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api_impl
import (
"crypto/x509"
"fmt"
"github.com/go-openapi/loads"
"github.com/gorilla/websocket"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/channel/v2"
"github.com/openziti/channel/v2/websockets"
"github.com/openziti/foundation/v2/concurrenz"
"github.com/openziti/identity"
"github.com/openziti/xweb/v2"
"github.com/openziti/ziti/controller/handler_mgmt"
"github.com/openziti/ziti/controller/network"
"github.com/openziti/ziti/controller/rest_client"
"github.com/openziti/ziti/controller/rest_server"
"github.com/openziti/ziti/controller/rest_server/operations"
"github.com/openziti/ziti/controller/xmgmt"
"net/http"
"strings"
)
const (
ServerHeader = "server"
)
var _ xweb.ApiHandlerFactory = &ManagementApiFactory{}
type ManagementApiFactory struct {
InitFunc func(managementApi *ManagementApiHandler) error
network *network.Network
nodeId identity.Identity
xmgmts *concurrenz.CopyOnWriteSlice[xmgmt.Xmgmt]
}
func (factory *ManagementApiFactory) Validate(_ *xweb.InstanceConfig) error {
return nil
}
func NewManagementApiFactory(nodeId identity.Identity, network *network.Network, xmgmts *concurrenz.CopyOnWriteSlice[xmgmt.Xmgmt]) *ManagementApiFactory {
pfxlog.Logger().Infof("initializing management api factory with %d xmgmt instances", len(xmgmts.Value()))
return &ManagementApiFactory{
network: network,
nodeId: nodeId,
xmgmts: xmgmts,
}
}
func (factory *ManagementApiFactory) Binding() string {
return FabricApiBinding
}
func (factory *ManagementApiFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
managementSpec, err := loads.Embedded(rest_server.SwaggerJSON, rest_server.FlatSwaggerJSON)
if err != nil {
pfxlog.Logger().Fatalln(err)
}
fabricAPI := operations.NewZitiFabricAPI(managementSpec)
fabricAPI.ServeError = ServeError
if requestWrapper == nil {
requestWrapper = &FabricRequestWrapper{
nodeId: factory.nodeId,
network: factory.network,
}
}
for _, router := range routers {
router.Register(fabricAPI, requestWrapper)
}
managementApiHandler, err := NewManagementApiHandler(fabricAPI, options)
if err != nil {
return nil, err
}
managementApiHandler.bindHandler = handler_mgmt.NewBindHandler(factory.network, factory.xmgmts)
if factory.InitFunc != nil {
if err := factory.InitFunc(managementApiHandler); err != nil {
return nil, fmt.Errorf("error running on init func: %v", err)
}
}
return managementApiHandler, nil
}
func NewManagementApiHandler(fabricApi *operations.ZitiFabricAPI, options map[interface{}]interface{}) (*ManagementApiHandler, error) {
managementApi := &ManagementApiHandler{
fabricApi: fabricApi,
options: options,
}
managementApi.handler = managementApi.newHandler()
managementApi.wsHandler = requestWrapper.WrapWsHandler(http.HandlerFunc(managementApi.handleWebSocket))
managementApi.wsUrl = rest_client.DefaultBasePath + "/ws-api"
return managementApi, nil
}
type ManagementApiHandler struct {
fabricApi *operations.ZitiFabricAPI
handler http.Handler
wsHandler http.Handler
wsUrl string
options map[interface{}]interface{}
bindHandler channel.BindHandler
}
func (managementApi *ManagementApiHandler) Binding() string {
return FabricApiBinding
}
func (managementApi *ManagementApiHandler) Options() map[interface{}]interface{} {
return managementApi.options
}
func (managementApi *ManagementApiHandler) RootPath() string {
return rest_client.DefaultBasePath
}
func (managementApi *ManagementApiHandler) IsHandler(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, managementApi.RootPath())
}
func (managementApi *ManagementApiHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path == managementApi.wsUrl {
managementApi.wsHandler.ServeHTTP(writer, request)
} else {
managementApi.handler.ServeHTTP(writer, request)
}
}
func (managementApi *ManagementApiHandler) newHandler() http.Handler {
innerManagementHandler := managementApi.fabricApi.Serve(nil)
return requestWrapper.WrapHttpHandler(innerManagementHandler)
}
func (managementApi *ManagementApiHandler) handleWebSocket(writer http.ResponseWriter, request *http.Request) {
log := pfxlog.Logger()
log.Debug("handling mgmt channel websocket upgrade")
upgrader := websocket.Upgrader{}
conn, err := upgrader.Upgrade(writer, request, nil)
if err != nil {
log.WithError(err).Error("unable to upgrade request to websocket")
return
}
var certs []*x509.Certificate
if request.TLS != nil {
certs = request.TLS.PeerCertificates
}
id := &identity.TokenId{Token: "mgmt"}
underlayFactory := websockets.NewUnderlayFactory(id, conn, certs)
_, err = channel.NewChannel("mgmt", underlayFactory, managementApi.bindHandler, nil)
if err != nil {
log.WithError(err).Error("unable to create channel over websocket")
return
}
}
-174
View File
@@ -1,174 +0,0 @@
/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api_impl
import (
"bytes"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/identity"
"github.com/openziti/xweb/v2"
"github.com/openziti/ziti/controller/network"
"net/http"
"os"
"strings"
)
var _ xweb.ApiHandlerFactory = &MetricsApiFactory{}
type MetricsApiFactory struct {
network *network.Network
nodeId identity.Identity
}
func (factory *MetricsApiFactory) Validate(_ *xweb.InstanceConfig) error {
return nil
}
func NewMetricsApiFactory(nodeId identity.Identity, network *network.Network) *MetricsApiFactory {
return &MetricsApiFactory{
network: network,
nodeId: nodeId,
}
}
func (factory *MetricsApiFactory) Binding() string {
return MetricApiBinding
}
func (factory *MetricsApiFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
metricsApiHandler, err := NewMetricsApiHandler(factory.network, options)
if err != nil {
return nil, err
}
return metricsApiHandler, nil
}
func NewMetricsApiHandler(n *network.Network, options map[interface{}]interface{}) (*MetricsApiHandler, error) {
metricsApi := &MetricsApiHandler{
options: options,
network: n,
inspectMgr: network.NewInspectionsManager(n),
}
if value, found := options["scrapeCert"]; found {
if f, ok := value.(string); ok {
p, err := os.ReadFile(f)
if nil != err {
return nil, err
}
block, _ := pem.Decode(p)
if block == nil {
err := errors.New("failed to decode metrics api scrapeCert")
return nil, err
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
err := errors.New("failed to parse certificate: " + err.Error())
return nil, err
}
metricsApi.scrapeCert = cert
} else {
return nil, errors.New("invalid configuration found for metrics pem. The scrapeCert must be a string")
}
} else {
pfxlog.Logger().Info("Metrics are enabled on /metrics, but no scrapeCert is provided in the controller configuration. Metrics are exposed without any authorization.")
}
includeTimestamps := false
if value, found := options["includeTimestamps"]; found {
if t, ok := value.(bool); ok {
includeTimestamps = t
pfxlog.Logger().Debugf("includeTimestamps set to %v in Prometheus metrics exporter", t)
}
}
metricsApi.modelMapper = NewMetricsModelMapper(n, "prometheus", includeTimestamps)
metricsApi.handler = metricsApi.newHandler()
return metricsApi, nil
}
type MetricsApiHandler struct {
inspectMgr *network.InspectionsManager
handler http.Handler
network *network.Network
scrapeCert *x509.Certificate
modelMapper MetricsModelMapper
options map[interface{}]interface{}
}
func (metricsApi *MetricsApiHandler) Binding() string {
return MetricApiBinding
}
func (metricsApi *MetricsApiHandler) Options() map[interface{}]interface{} {
return metricsApi.options
}
func (metricsApi *MetricsApiHandler) RootPath() string {
return "/metrics"
}
func (metricsApi *MetricsApiHandler) IsHandler(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, metricsApi.RootPath())
}
func (metricsApi *MetricsApiHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
metricsApi.handler.ServeHTTP(writer, request)
}
func (metricsApi *MetricsApiHandler) newHandler() http.Handler {
handler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if nil != metricsApi.scrapeCert {
certOk := false
for _, r := range r.TLS.PeerCertificates {
if bytes.Equal(metricsApi.scrapeCert.Signature, r.Signature) {
certOk = true
}
}
if !certOk {
rw.WriteHeader(http.StatusUnauthorized)
return
}
}
inspection := metricsApi.inspectMgr.Inspect(".*", []string{"metrics:prometheus"})
metricsResult, err := metricsApi.modelMapper.MapInspectResultToMetricsResult(inspection)
if err != nil {
_, _ = rw.Write([]byte(fmt.Sprintf("Failed to convert metrics to prometheus format %s:%s", metricsApi.network.GetAppId(), err.Error())))
rw.WriteHeader(http.StatusInternalServerError)
} else {
if _, err = rw.Write([]byte(*metricsResult)); err != nil {
rw.WriteHeader(http.StatusInternalServerError)
}
}
})
return handler
}
+4 -4
View File
@@ -1,9 +1,9 @@
package api_impl
import (
"github.com/openziti/foundation/v2/errorz"
"github.com/openziti/ziti/controller/api"
"github.com/openziti/ziti/controller/rest_model"
"github.com/openziti/foundation/v2/errorz"
"net/http"
)
@@ -28,16 +28,16 @@ func RespondWithOk(responder api.Responder, data interface{}, meta *rest_model.M
}, http.StatusOK)
}
type fabricResponseMapper struct{}
type FabricResponseMapper struct{}
func (self fabricResponseMapper) EmptyOkData() interface{} {
func (self FabricResponseMapper) EmptyOkData() interface{} {
return &rest_model.Empty{
Data: map[string]interface{}{},
Meta: &rest_model.Meta{},
}
}
func (self fabricResponseMapper) MapApiError(requestId string, apiError *errorz.ApiError) interface{} {
func (self FabricResponseMapper) MapApiError(requestId string, apiError *errorz.ApiError) interface{} {
return &rest_model.APIErrorEnvelope{
Error: ToRestModel(apiError, requestId),
Meta: &rest_model.Meta{
+5 -3
View File
@@ -1,11 +1,13 @@
package api_impl
import "github.com/openziti/ziti/controller/rest_server/operations"
import (
"github.com/openziti/ziti/controller/rest_server/operations"
)
var routers []Router
var Routers []Router
func AddRouter(router Router) {
routers = append(routers, router)
Routers = append(Routers, router)
}
type Router interface {
+2 -120
View File
@@ -1,134 +1,16 @@
package api_impl
import (
"crypto/x509"
"github.com/go-openapi/runtime"
openApiMiddleware "github.com/go-openapi/runtime/middleware"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/ziti/common/build"
"github.com/go-openapi/runtime/middleware"
"github.com/openziti/ziti/controller/api"
"github.com/openziti/ziti/controller/apierror"
"github.com/openziti/ziti/controller/network"
"github.com/openziti/ziti/controller/rest_server"
"github.com/openziti/foundation/v2/errorz"
"github.com/openziti/identity"
"github.com/pkg/errors"
"net/http"
"time"
)
var requestWrapper RequestWrapper
func OverrideRequestWrapper(rw RequestWrapper) {
if requestWrapper != nil {
pfxlog.Logger().Warn("requestWrapper overridden more than once")
}
requestWrapper = rw
}
type RequestHandler func(network *network.Network, rc api.RequestContext)
type RequestWrapper interface {
WrapRequest(handler RequestHandler, request *http.Request, entityId, entitySubId string) openApiMiddleware.Responder
WrapRequest(handler RequestHandler, request *http.Request, entityId, entitySubId string) middleware.Responder
WrapHttpHandler(handler http.Handler) http.Handler
WrapWsHandler(handler http.Handler) http.Handler
}
type FabricRequestWrapper struct {
nodeId identity.Identity
network *network.Network
}
func (self *FabricRequestWrapper) WrapRequest(handler RequestHandler, request *http.Request, entityId, entitySubId string) openApiMiddleware.Responder {
return openApiMiddleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) {
rc, err := api.GetRequestContextFromHttpContext(request)
if rc == nil {
rc = NewRequestContext(writer, request)
}
rc.SetProducer(producer)
rc.SetEntityId(entityId)
rc.SetEntitySubId(entitySubId)
if err != nil {
pfxlog.Logger().WithError(err).Error("could not retrieve request context")
rc.RespondWithError(err)
return
}
handler(self.network, rc)
})
}
func (self *FabricRequestWrapper) WrapHttpHandler(handler http.Handler) http.Handler {
wrapper := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == FabricRestApiSpecUrl {
rw.Header().Set("content-type", "application/json")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(rest_server.SwaggerJSON)
return
}
rc := NewRequestContext(rw, r)
if err := self.verifyCert(r); err != nil {
rc.RespondWithError(apierror.NewInvalidAuth())
return
}
api.AddRequestContextToHttpContext(r, rc)
//after request context is filled so that api session is present for session expiration headers
buildInfo := build.GetBuildInfo()
if buildInfo != nil {
rc.GetResponseWriter().Header().Set(ServerHeader, "ziti-controller/"+buildInfo.Version())
}
handler.ServeHTTP(rw, r)
})
return api.TimeoutHandler(api.WrapCorsHandler(wrapper), 10*time.Second, apierror.NewTimeoutError(), fabricResponseMapper{})
}
func (self *FabricRequestWrapper) WrapWsHandler(handler http.Handler) http.Handler {
wrapper := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if err := self.verifyCert(r); err != nil {
rc := NewRequestContext(rw, r)
rc.RespondWithError(apierror.NewInvalidAuth())
return
}
handler.ServeHTTP(rw, r)
})
return wrapper
}
func (self *FabricRequestWrapper) verifyCert(r *http.Request) error {
certificates := r.TLS.PeerCertificates
if len(certificates) == 0 {
return errors.New("no certificates provided, unable to verify dialer")
}
config := self.nodeId.ServerTLSConfig()
opts := x509.VerifyOptions{
Roots: config.RootCAs,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
var errorList errorz.MultipleErrors
for _, cert := range certificates {
if _, err := cert.Verify(opts); err == nil {
return nil
} else {
errorList = append(errorList, err)
}
}
//goland:noinspection GoNilness
return errorList.ToError()
}