瀏覽代碼

add cluster middleware

Alexander Belanger 4 年之前
父節點
當前提交
82a9921f23

+ 53 - 0
api/server/authz/cluster.go

@@ -0,0 +1,53 @@
+package authz
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz/policy"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ClusterScopedFactory struct {
+	config *shared.Config
+}
+
+func NewClusterScopedFactory(
+	config *shared.Config,
+) *ClusterScopedFactory {
+	return &ClusterScopedFactory{config}
+}
+
+func (p *ClusterScopedFactory) Middleware(next http.Handler) http.Handler {
+	return &ClusterScopedMiddleware{next, p.config}
+}
+
+type ClusterScopedMiddleware struct {
+	next   http.Handler
+	config *shared.Config
+}
+
+func (p *ClusterScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// get the project id from the URL param context
+	reqScopes, _ := r.Context().Value(RequestScopeCtxKey).(map[types.PermissionScope]*policy.RequestAction)
+
+	clusterID := reqScopes[types.ClusterScope].Resource.UInt
+
+	cluster, err := p.config.Repo.Cluster().ReadCluster(clusterID)
+
+	if err != nil {
+		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	ctx := NewClusterContext(r.Context(), cluster)
+	r = r.WithContext(ctx)
+	p.next.ServeHTTP(w, r)
+}
+
+func NewClusterContext(ctx context.Context, cluster *models.Cluster) context.Context {
+	return context.WithValue(ctx, types.ClusterScope, cluster)
+}

+ 29 - 0
api/server/handlers/cluster/agent.go

@@ -0,0 +1,29 @@
+package cluster
+
+import (
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type KubernetesAgentGetter interface {
+	GetAgent(cluster *models.Cluster) (*kubernetes.Agent, error)
+}
+
+type DefaultKubernetesAgentGetter struct {
+	config *shared.Config
+}
+
+func NewDefaultKubernetesAgentGetter(config *shared.Config) KubernetesAgentGetter {
+	return &DefaultKubernetesAgentGetter{config}
+}
+
+func (d *DefaultKubernetesAgentGetter) GetAgent(cluster *models.Cluster) (*kubernetes.Agent, error) {
+	ooc := &kubernetes.OutOfClusterConfig{
+		Repo:              d.config.Repo,
+		DigitalOceanOAuth: d.config.DOConf,
+		Cluster:           cluster,
+	}
+
+	return kubernetes.GetAgentOutOfClusterConfig(ooc)
+}

+ 55 - 0
api/server/handlers/cluster/get.go

@@ -0,0 +1,55 @@
+package cluster
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/kubernetes/domain"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ClusterGetHandler struct {
+	handlers.PorterHandlerWriter
+	KubernetesAgentGetter
+}
+
+func NewClusterGetHandler(
+	config *shared.Config,
+	writer shared.ResultWriter,
+) *ClusterGetHandler {
+	return &ClusterGetHandler{
+		PorterHandlerWriter:   handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter: NewDefaultKubernetesAgentGetter(config),
+	}
+}
+
+func (c *ClusterGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	res := &types.ClusterGetResponse{
+		Cluster: cluster.ToClusterType(),
+	}
+
+	agent, err := c.GetAgent(cluster)
+
+	if err != nil {
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		return
+	}
+
+	endpoint, found, ingressErr := domain.GetNGINXIngressServiceIP(agent.Clientset)
+
+	if found {
+		res.IngressIP = endpoint
+	}
+
+	if !found && ingressErr != nil {
+		res.IngressError = kubernetes.CatchK8sConnectionError(ingressErr).Externalize()
+	}
+
+	c.WriteResult(w, res)
+}

+ 45 - 0
api/server/handlers/cluster/list.go

@@ -0,0 +1,45 @@
+package cluster
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ClusterListHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewClusterListHandler(
+	config *shared.Config,
+	writer shared.ResultWriter,
+) *ClusterListHandler {
+	return &ClusterListHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ClusterListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the project from context
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	// read all clusters for this project
+	clusters, err := p.Repo().Cluster().ListClustersByProjectID(proj.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make([]*types.Cluster, len(clusters))
+
+	for i, cluster := range clusters {
+		res[i] = cluster.ToClusterType()
+	}
+
+	p.WriteResult(w, res)
+}

+ 83 - 0
api/server/router/cluster.go

@@ -0,0 +1,83 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/cluster"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewClusterScopedRegisterer(children ...*Registerer) *Registerer {
+	return &Registerer{
+		GetRoutes: GetClusterScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetClusterScopedRoutes(
+	r chi.Router,
+	config *shared.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*Registerer,
+) []*Route {
+	routes, projPath := getClusterRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getClusterRoutes(
+	r chi.Router,
+	config *shared.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*Route, *types.Path) {
+	relPath := "/clusters/{cluster_id}"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	routes := make([]*Route, 0)
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id} -> project.NewClusterGetHandler
+	getEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getHandler := cluster.NewClusterGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getEndpoint,
+		Handler:  getHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 32 - 15
api/server/router/project.go

@@ -2,6 +2,7 @@ package router
 
 import (
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
@@ -33,17 +34,6 @@ func GetProjectScopedRoutes(
 		})
 	}
 
-	// // Note that we can place the middleware r.Use below
-
-	// // all project-scoped routes
-
-	// // Create a new "project-scoped" factory which will create a new project-scoped request
-	// // after authorization. Each subsequent http.Handler can lookup the project in context.
-	// projFactory := authz.NewProjectScopedFactory(config)
-
-	// // attach middleware to router
-	// r.Use(projFactory.Middleware)
-
 	return routes
 }
 
@@ -62,10 +52,10 @@ func getProjectRoutes(
 
 	routes := make([]*Route, 0)
 
-	// POST /api/projects -> project.NewProjectCreateHandler
+	// GET /api/projects/{project_id} -> project.NewProjectGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
+			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
@@ -78,14 +68,41 @@ func getProjectRoutes(
 		},
 	)
 
-	createHandler := project.NewProjectGetHandler(
+	getHandler := project.NewProjectGetHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
 		Endpoint: getEndpoint,
-		Handler:  createHandler,
+		Handler:  getHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters -> cluster.NewClusterListHandler
+	listClusterEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/clusters",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listClusterHandler := cluster.NewClusterListHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listClusterEndpoint,
+		Handler:  listClusterHandler,
 		Router:   r,
 	})
 

+ 15 - 1
api/server/router/router.go

@@ -6,6 +6,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/authn"
 	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
 )
@@ -18,7 +19,8 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 
 	endpointFactory := shared.NewAPIObjectEndpointFactory(config)
 	baseRegisterer := NewBaseRegisterer()
-	projRegisterer := NewProjectScopedRegisterer()
+	clusterRegisterer := NewClusterScopedRegisterer()
+	projRegisterer := NewProjectScopedRegisterer(clusterRegisterer)
 	userRegisterer := NewUserScopedRegisterer(projRegisterer)
 
 	r.Route("/api", func(r chi.Router) {
@@ -76,6 +78,13 @@ func registerRoutes(config *shared.Config, routes []*Route) {
 	// after authorization. Each subsequent http.Handler can lookup the project in context.
 	projFactory := authz.NewProjectScopedFactory(config)
 
+	// Create a new "cluster-scoped" factory which will create a new cluster-scoped request
+	// after authorization. Each subsequent http.Handler can lookup the cluster in context.
+	clusterFactory := authz.NewClusterScopedFactory(config)
+
+	// Policy doc loader loads the policy documents for a specific project.
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
+
 	for _, route := range routes {
 		atomicGroup := route.Router.Group(nil)
 
@@ -89,7 +98,12 @@ func registerRoutes(config *shared.Config, routes []*Route) {
 					atomicGroup.Use(authNFactory.NewAuthenticated)
 				}
 			case types.ProjectScope:
+				policyFactory := authz.NewPolicyMiddleware(config, *route.Endpoint.Metadata, policyDocLoader)
+
+				atomicGroup.Use(policyFactory.Middleware)
 				atomicGroup.Use(projFactory.Middleware)
+			case types.ClusterScope:
+				atomicGroup.Use(clusterFactory.Middleware)
 			}
 		}
 

+ 22 - 12
api/server/router/router_test.go

@@ -1,16 +1,26 @@
 package router_test
 
-// func TestRouter(t *testing.T) {
-// 	walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
-// 		route = strings.Replace(route, "/*/", "/", -1)
-// 		t.Errorf("%s %s %d\n", method, route, len(middlewares))
-// 		return nil
-// 	}
+import (
+	"net/http"
+	"strings"
+	"testing"
 
-// 	config := apitest.LoadConfig(t)
-// 	r := router.NewAPIRouter(config)
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/router"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
+)
 
-// 	if err := chi.Walk(r, walkFunc); err != nil {
-// 		t.Fatalf("Logging err: %s\n", err.Error())
-// 	}
-// }
+func TestRouter(t *testing.T) {
+	walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
+		route = strings.Replace(route, "/*/", "/", -1)
+		t.Errorf("%s %s %d\n", method, route, len(middlewares))
+		return nil
+	}
+
+	config := apitest.LoadConfig(t)
+	r := router.NewAPIRouter(config)
+
+	if err := chi.Walk(r, walkFunc); err != nil {
+		t.Fatalf("Logging err: %s\n", err.Error())
+	}
+}

+ 4 - 0
api/server/shared/config.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 )
 
 type Config struct {
@@ -33,6 +34,9 @@ type Config struct {
 	// UserNotifier is an object that notifies users of transactions (pw reset, email
 	// verification, etc)
 	UserNotifier notifier.UserNotifier
+
+	// DOConf is the configuration for a DigitalOcean OAuth client
+	DOConf *oauth2.Config
 }
 
 type ConfigLoader interface {

+ 42 - 0
api/types/cluster.go

@@ -0,0 +1,42 @@
+package types
+
+type Cluster struct {
+	ID uint `json:"id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// Name of the cluster
+	Name string `json:"name"`
+
+	// Server endpoint for the cluster
+	Server string `json:"server"`
+
+	// The integration service for this cluster
+	Service ClusterService `json:"service"`
+
+	// The infra id, if cluster was provisioned with Porter
+	InfraID uint `json:"infra_id"`
+
+	// (optional) The aws integration id, if available
+	AWSIntegrationID uint `json:"aws_integration_id"`
+}
+
+type ClusterGetResponse struct {
+	*Cluster
+
+	// The NGINX Ingress IP to access the cluster
+	IngressIP string `json:"ingress_ip"`
+
+	// Error displayed in case couldn't get the IP
+	IngressError error `json:"ingress_error"`
+}
+
+type ClusterService string
+
+const (
+	EKS  ClusterService = "eks"
+	DOKS ClusterService = "doks"
+	GKE  ClusterService = "gke"
+	Kube ClusterService = "kube"
+)

+ 0 - 4
api/types/policy.go

@@ -26,10 +26,6 @@ type PolicyDocument struct {
 type ScopeTree map[PermissionScope]ScopeTree
 
 /* ScopeHeirarchy describes the scope tree:
-<<<<<<< HEAD
-
-=======
->>>>>>> master
 			Project
 		   /	   \
 		Cluster   Settings

+ 24 - 0
internal/models/cluster.go

@@ -3,6 +3,7 @@ package models
 import (
 	"encoding/json"
 
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"gorm.io/gorm"
 )
@@ -114,6 +115,29 @@ func (c *Cluster) Externalize() *ClusterExternal {
 	}
 }
 
+// ToProjectType generates an external types.Project to be shared over REST
+func (c *Cluster) ToClusterType() *types.Cluster {
+	serv := types.Kube
+
+	if c.AWSIntegrationID != 0 {
+		serv = types.EKS
+	} else if c.GCPIntegrationID != 0 {
+		serv = types.GKE
+	} else if c.DOIntegrationID != 0 {
+		serv = types.DOKS
+	}
+
+	return &types.Cluster{
+		ID:               c.ID,
+		ProjectID:        c.ProjectID,
+		Name:             c.Name,
+		Server:           c.Server,
+		Service:          serv,
+		InfraID:          c.InfraID,
+		AWSIntegrationID: c.AWSIntegrationID,
+	}
+}
+
 type ClusterDetailedExternal struct {
 	// Simple cluster external data
 	ClusterExternal

+ 0 - 22
internal/models/project.go

@@ -42,28 +42,6 @@ type Project struct {
 	GCPIntegrations   []ints.GCPIntegration   `json:"gcp_integrations"`
 }
 
-// // ProjectExternal represents the Project type that is sent over REST
-// type ProjectExternal struct {
-// 	ID       uint              `json:"id"`
-// 	Name     string            `json:"name"`
-// 	GitRepos []GitRepoExternal `json:"git_repos,omitempty"`
-// }
-
-// // Externalize generates an external Project to be shared over REST
-// func (p *Project) Externalize() *ProjectExternal {
-// 	roles := make([]RoleExternal, 0)
-
-// 	for _, role := range p.Roles {
-// 		roles = append(roles, *role.Externalize())
-// 	}
-
-// 	return &ProjectExternal{
-// 		ID:    p.ID,
-// 		Name:  p.Name,
-// 		Roles: roles,
-// 	}
-// }
-
 // ToProjectType generates an external types.Project to be shared over REST
 func (p *Project) ToProjectType() *types.Project {
 	roles := make([]*types.Role, 0)