Support for prefixed Orchard Controller API URLs (#355)
* Support for prefixed Orchard Controller API URLs * Fix Swagger UI * Remove spurious "fmt" import * Use url.URL in order to correctly calculate API path for Swagger UI
This commit is contained in:
parent
6d23548d81
commit
af221cf3c1
|
|
@ -24,6 +24,7 @@ import (
|
||||||
var ErrRunFailed = errors.New("failed to run controller")
|
var ErrRunFailed = errors.New("failed to run controller")
|
||||||
|
|
||||||
var address string
|
var address string
|
||||||
|
var apiPrefix string
|
||||||
var addressSSH string
|
var addressSSH string
|
||||||
var addressPprof string
|
var addressPprof string
|
||||||
var debug bool
|
var debug bool
|
||||||
|
|
@ -50,6 +51,9 @@ func newRunCommand() *cobra.Command {
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&address, "listen", "l", fmt.Sprintf(":%s", port),
|
cmd.Flags().StringVarP(&address, "listen", "l", fmt.Sprintf(":%s", port),
|
||||||
"address to listen on")
|
"address to listen on")
|
||||||
|
cmd.Flags().StringVar(&apiPrefix, "api-prefix", "",
|
||||||
|
"prefix to prepend to all Orchard Controller API endpoints; useful when exposing Orchard Controller "+
|
||||||
|
"behind an HTTP proxy together with other services")
|
||||||
cmd.Flags().StringVar(&addressSSH, "listen-ssh", "",
|
cmd.Flags().StringVar(&addressSSH, "listen-ssh", "",
|
||||||
"address for the built-in SSH server to listen on (e.g. \":6122\")")
|
"address for the built-in SSH server to listen on (e.g. \":6122\")")
|
||||||
cmd.Flags().StringVar(&addressPprof, "listen-pprof", "",
|
cmd.Flags().StringVar(&addressPprof, "listen-pprof", "",
|
||||||
|
|
@ -144,6 +148,10 @@ func runController(cmd *cobra.Command, args []string) (err error) {
|
||||||
controller.WithLogger(logger),
|
controller.WithLogger(logger),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if apiPrefix != "" {
|
||||||
|
controllerOpts = append(controllerOpts, controller.WithAPIPrefix(apiPrefix))
|
||||||
|
}
|
||||||
|
|
||||||
var controllerCert tls.Certificate
|
var controllerCert tls.Certificate
|
||||||
|
|
||||||
if !noTLS {
|
if !noTLS {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ package dev
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/cirruslabs/orchard/internal/config"
|
"github.com/cirruslabs/orchard/internal/config"
|
||||||
"github.com/cirruslabs/orchard/internal/controller"
|
"github.com/cirruslabs/orchard/internal/controller"
|
||||||
"github.com/cirruslabs/orchard/internal/netconstants"
|
"github.com/cirruslabs/orchard/internal/netconstants"
|
||||||
|
|
@ -13,14 +17,12 @@ import (
|
||||||
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrFailed = errors.New("failed to run development controller and worker")
|
var ErrFailed = errors.New("failed to run development controller and worker")
|
||||||
|
|
||||||
var devDataDirPath string
|
var devDataDirPath string
|
||||||
|
var apiPrefix string
|
||||||
var stringToStringResources map[string]string
|
var stringToStringResources map[string]string
|
||||||
var experimentalRPCV2 bool
|
var experimentalRPCV2 bool
|
||||||
|
|
||||||
|
|
@ -33,6 +35,9 @@ func NewCommand() *cobra.Command {
|
||||||
|
|
||||||
command.Flags().StringVarP(&devDataDirPath, "data-dir", "d", ".dev-data",
|
command.Flags().StringVarP(&devDataDirPath, "data-dir", "d", ".dev-data",
|
||||||
"path to persist data between runs")
|
"path to persist data between runs")
|
||||||
|
command.Flags().StringVar(&apiPrefix, "api-prefix", "",
|
||||||
|
"prefix to prepend to all Orchard Controller API endpoints; useful when exposing Orchard Controller "+
|
||||||
|
"behind an HTTP proxy together with other services")
|
||||||
command.Flags().StringToStringVar(&stringToStringResources, "resources", map[string]string{},
|
command.Flags().StringToStringVar(&stringToStringResources, "resources", map[string]string{},
|
||||||
"resources that the development worker will provide")
|
"resources that the development worker will provide")
|
||||||
command.Flags().BoolVar(&experimentalRPCV2, "experimental-rpc-v2", false,
|
command.Flags().BoolVar(&experimentalRPCV2, "experimental-rpc-v2", false,
|
||||||
|
|
@ -58,6 +63,10 @@ func runDev(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
var additionalControllerOpts []controller.Option
|
var additionalControllerOpts []controller.Option
|
||||||
|
|
||||||
|
if apiPrefix != "" {
|
||||||
|
additionalControllerOpts = append(additionalControllerOpts, controller.WithAPIPrefix(apiPrefix))
|
||||||
|
}
|
||||||
|
|
||||||
if experimentalRPCV2 {
|
if experimentalRPCV2 {
|
||||||
additionalControllerOpts = append(additionalControllerOpts, controller.WithExperimentalRPCV2())
|
additionalControllerOpts = append(additionalControllerOpts, controller.WithExperimentalRPCV2())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cirruslabs/orchard/api"
|
"github.com/cirruslabs/orchard/api"
|
||||||
|
|
@ -28,7 +29,15 @@ var ErrUnauthorized = errors.New("unauthorized")
|
||||||
func (controller *Controller) initAPI() *gin.Engine {
|
func (controller *Controller) initAPI() *gin.Engine {
|
||||||
ginEngine := gin.New()
|
ginEngine := gin.New()
|
||||||
|
|
||||||
ginEngine.Use(
|
var group *gin.RouterGroup
|
||||||
|
|
||||||
|
if controller.apiPrefix != "" {
|
||||||
|
group = ginEngine.Group(controller.apiPrefix)
|
||||||
|
} else {
|
||||||
|
group = ginEngine.Group("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Use(
|
||||||
ginzap.Ginzap(controller.logger.Desugar(), "", true),
|
ginzap.Ginzap(controller.logger.Desugar(), "", true),
|
||||||
ginzap.RecoveryWithZap(controller.logger.Desugar(), true),
|
ginzap.RecoveryWithZap(controller.logger.Desugar(), true),
|
||||||
)
|
)
|
||||||
|
|
@ -36,10 +45,10 @@ func (controller *Controller) initAPI() *gin.Engine {
|
||||||
// expose metrics
|
// expose metrics
|
||||||
monitor := ginmetrics.GetMonitor()
|
monitor := ginmetrics.GetMonitor()
|
||||||
monitor.SetMetricPath("/metrics")
|
monitor.SetMetricPath("/metrics")
|
||||||
monitor.Use(ginEngine)
|
monitor.Use(group)
|
||||||
|
|
||||||
// v1 API
|
// v1 API
|
||||||
v1 := ginEngine.Group("/v1")
|
v1 := group.Group("/v1")
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
v1.Use(controller.authenticateMiddleware)
|
v1.Use(controller.authenticateMiddleware)
|
||||||
|
|
@ -48,9 +57,14 @@ func (controller *Controller) initAPI() *gin.Engine {
|
||||||
// to check that the API is working
|
// to check that the API is working
|
||||||
v1.GET("/", func(c *gin.Context) {
|
v1.GET("/", func(c *gin.Context) {
|
||||||
if controller.enableSwaggerDocs {
|
if controller.enableSwaggerDocs {
|
||||||
|
apiURL := &url.URL{
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
apiURL = apiURL.JoinPath(controller.apiPrefix, "v1")
|
||||||
|
|
||||||
middleware.SwaggerUI(middleware.SwaggerUIOpts{
|
middleware.SwaggerUI(middleware.SwaggerUIOpts{
|
||||||
Path: "/v1",
|
Path: apiURL.Path,
|
||||||
SpecURL: "/v1/openapi.yaml",
|
SpecURL: apiURL.JoinPath("openapi.yaml").Path,
|
||||||
}, nil).ServeHTTP(c.Writer, c.Request)
|
}, nil).ServeHTTP(c.Writer, c.Request)
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -46,6 +47,7 @@ const (
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
dataDir *DataDir
|
dataDir *DataDir
|
||||||
listenAddr string
|
listenAddr string
|
||||||
|
apiPrefix string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
|
@ -262,11 +264,17 @@ func (controller *Controller) Run(ctx context.Context) error {
|
||||||
func (controller *Controller) Address() string {
|
func (controller *Controller) Address() string {
|
||||||
hostPort := strings.ReplaceAll(controller.listener.Addr().String(), "[::]", "127.0.0.1")
|
hostPort := strings.ReplaceAll(controller.listener.Addr().String(), "[::]", "127.0.0.1")
|
||||||
|
|
||||||
if controller.tlsConfig != nil {
|
url := url.URL{
|
||||||
return fmt.Sprintf("https://%s", hostPort)
|
Scheme: "http",
|
||||||
|
Host: hostPort,
|
||||||
|
Path: controller.apiPrefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("http://%s", hostPort)
|
if controller.tlsConfig != nil {
|
||||||
|
url.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *Controller) SSHAddress() (string, bool) {
|
func (controller *Controller) SSHAddress() (string, bool) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ func WithListenAddr(listenAddr string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithAPIPrefix(apiPrefix string) Option {
|
||||||
|
return func(c *Controller) {
|
||||||
|
c.apiPrefix = apiPrefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithTLSConfig(tlsConfig *tls.Config) Option {
|
func WithTLSConfig(tlsConfig *tls.Config) Option {
|
||||||
return func(controller *Controller) {
|
return func(controller *Controller) {
|
||||||
controller.tlsConfig = tlsConfig
|
controller.tlsConfig = tlsConfig
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package netconstants_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cirruslabs/orchard/internal/netconstants"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeAddress(t *testing.T) {
|
||||||
|
// Default port
|
||||||
|
url, err := netconstants.NormalizeAddress("subdomain.example.com/some/prefix")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "subdomain.example.com:6120", url.Host)
|
||||||
|
require.Equal(t, "/some/prefix", url.Path)
|
||||||
|
|
||||||
|
// Custom port
|
||||||
|
url, err = netconstants.NormalizeAddress("subdomain.example.com:443/some/prefix")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "subdomain.example.com:443", url.Host)
|
||||||
|
require.Equal(t, "/some/prefix", url.Path)
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cirruslabs/orchard/internal/config"
|
"github.com/cirruslabs/orchard/internal/config"
|
||||||
"github.com/cirruslabs/orchard/internal/version"
|
"github.com/cirruslabs/orchard/internal/version"
|
||||||
"github.com/cirruslabs/orchard/rpc"
|
"github.com/cirruslabs/orchard/rpc"
|
||||||
|
|
@ -16,11 +22,6 @@ import (
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -298,6 +299,7 @@ func (client *Client) formatPath(path string) *url.URL {
|
||||||
Scheme: client.baseURL.Scheme,
|
Scheme: client.baseURL.Scheme,
|
||||||
User: client.baseURL.User,
|
User: client.baseURL.User,
|
||||||
Host: client.baseURL.Host,
|
Host: client.baseURL.Host,
|
||||||
|
Path: client.baseURL.Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpointURL.JoinPath("v1", path)
|
return endpointURL.JoinPath("v1", path)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue