Просмотр исходного кода

feat: implement routes needed for custom datastore frontend (#4078)

jose-fully-ported 2 лет назад
Родитель
Сommit
32fd0aa728

+ 94 - 0
api/server/handlers/cloud_provider/list_aws.go

@@ -0,0 +1,94 @@
+package cloud_provider
+
+import (
+	"net/http"
+
+	"github.com/aws/aws-sdk-go/aws/arn"
+	"github.com/porter-dev/porter/api/server/authz"
+	"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/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// ListAwsAccountsResponse describes an outbound response for listing aws accounts on
+// a given project.
+type ListAwsAccountsResponse struct {
+	// Accounts is a list of aws account objects
+	Accounts []AwsAccount `json:"accounts"`
+}
+
+// AwsAccount describes an outbound response for listing aws accounts on
+// a given project.
+//
+// The shape of the object is "generic" as there will be similar endpoints in
+// the future for other cloud providers.
+type AwsAccount struct {
+	// CloudProviderID is the cloud provider id - for AWS, this is an account
+	CloudProviderID string `json:"cloud_provider_id"`
+
+	// ProjectID is the project the account is associated with
+	ProjectID uint `json:"project_id"`
+}
+
+// ListAwsAccountsHandler is a struct for handling an aws cloud provider list request
+type ListAwsAccountsHandler struct {
+	handlers.PorterHandlerWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewListAwsAccountsHandler constructs a ListAwsAccountsHandler
+func NewListAwsAccountsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListAwsAccountsHandler {
+	return &ListAwsAccountsHandler{
+		PorterHandlerWriter:   handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// ServeHTTP returns a list of AWS Accounts
+//
+// todo: Move this logic down into CCP
+func (c *ListAwsAccountsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-cloud-provider-list-aws")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	res := ListAwsAccountsResponse{
+		Accounts: []AwsAccount{},
+	}
+	if !project.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) {
+		err := telemetry.Error(ctx, span, nil, "listing cloud providers not available on non-capi clusters")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	dblinks, err := c.Repo().AWSAssumeRoleChainer().List(ctx, project.ID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "unable to find assume role chain links")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	for _, link := range dblinks {
+		targetArn, err := arn.Parse(link.TargetARN)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "err-target-arn", Value: link.TargetARN})
+			err := telemetry.Error(ctx, span, err, "unable to parse target arn")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		res.Accounts = append(res.Accounts, AwsAccount{
+			CloudProviderID: targetArn.AccountID,
+			ProjectID:       uint(link.ProjectID),
+		})
+	}
+	c.WriteResult(w, r, res)
+}

+ 175 - 0
api/server/handlers/datastore/list.go

@@ -0,0 +1,175 @@
+package datastore
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/api/server/authz"
+	"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/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// ListDatastoresRequest is a struct that represents the various filter options used for
+// retrieving the datastores
+type ListDatastoresRequest struct {
+	// Name is the name of the datastore to filter by
+	Name string `schema:"name"`
+
+	// Type is the type of the datastore to filter by
+	Type string `schema:"type"`
+
+	// IncludeEnvGroup controls whether to include the datastore env group or not
+	IncludeEnvGroup bool `schema:"include_env_group"`
+
+	// IncludeMetadata controls whether to include datastore metadata or not
+	IncludeMetadata bool `schema:"include_metadata"`
+}
+
+// ListDatastoresResponse describes the list datastores response body
+type ListDatastoresResponse struct {
+	// Datastores is a list of datastore entries for the http response
+	Datastores []DatastoresResponseEntry `json:"datastores"`
+}
+
+// DatastoresResponseEntry describes an outbound datastores response entry
+type DatastoresResponseEntry struct {
+	// Name is the name of the datastore
+	Name string `json:"name"`
+
+	// Type is the type of the datastore
+	Type string `json:"type"`
+
+	// Env is the env group for the datastore
+	Env *porterv1.EnvGroup `json:"env,omitempty"`
+
+	// Metadata is a list of metadata objects for the datastore
+	Metadata []*porterv1.DatastoreMetadata `json:"metadata,omitempty"`
+
+	// Status is the status of the datastore
+	Status string `json:"status,omitempty"`
+}
+
+// ListDatastoresHandler is a struct for handling datastore status requests
+type ListDatastoresHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewListDatastoresHandler constructs a datastore ListHandler
+func NewListDatastoresHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListDatastoresHandler {
+	return &ListDatastoresHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// ServeHTTP returns a list of datastores associated with the specified project/cloud-provider
+func (h *ListDatastoresHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-datastore-list")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	request := &ListDatastoresRequest{}
+	if ok := h.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	cloudProviderType, err := requestutils.GetURLParamString(r, types.URLParamCloudProviderType)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing cloud provider type")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cloud-provider-type", Value: cloudProviderType})
+
+	cloudProviderID, err := requestutils.GetURLParamString(r, types.URLParamCloudProviderID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing cloud provider id")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cloud-provider-id", Value: cloudProviderID})
+
+	datastoreType := porterv1.EnumDatastore_ENUM_DATASTORE_UNSPECIFIED
+	switch request.Type {
+	case "elasticache-redis":
+		datastoreType = porterv1.EnumDatastore_ENUM_DATASTORE_ELASTICACHE_REDIS
+	case "rds-postgresql":
+		datastoreType = porterv1.EnumDatastore_ENUM_DATASTORE_RDS_POSTGRESQL
+	case "rds-postgresql-aurora":
+		datastoreType = porterv1.EnumDatastore_ENUM_DATASTORE_RDS_AURORA_POSTGRESQL
+	case "":
+		datastoreType = porterv1.EnumDatastore_ENUM_DATASTORE_UNSPECIFIED
+	default:
+		err := telemetry.Error(ctx, span, err, "invalid datastore type specified")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "datastore-name", Value: request.Name},
+		telemetry.AttributeKV{Key: "datastore-type", Value: request.Type},
+		telemetry.AttributeKV{Key: "include-env-group", Value: request.IncludeEnvGroup},
+		telemetry.AttributeKV{Key: "include-metadata", Value: request.IncludeMetadata},
+	)
+
+	var cloudProvider porterv1.EnumCloudProvider
+	switch cloudProviderType {
+	case "aws":
+		cloudProvider = porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS
+	default:
+		err := telemetry.Error(ctx, span, nil, "unsupported cloud provider")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	message := porterv1.ListDatastoresRequest{
+		ProjectId:              int64(project.ID),
+		CloudProvider:          cloudProvider,
+		CloudProviderAccountId: cloudProviderID,
+		Name:                   request.Name,
+		IncludeEnvGroup:        request.IncludeEnvGroup,
+		IncludeMetadata:        request.IncludeMetadata,
+	}
+	if datastoreType != porterv1.EnumDatastore_ENUM_DATASTORE_UNSPECIFIED {
+		message.Type = &datastoreType
+	}
+	req := connect.NewRequest(&message)
+	resp, ccpErr := h.Config().ClusterControlPlaneClient.ListDatastores(ctx, req)
+	if ccpErr != nil {
+		err := telemetry.Error(ctx, span, ccpErr, "error listing datastores from ccp")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if resp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "missing response message from ccp")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	response := ListDatastoresResponse{
+		Datastores: []DatastoresResponseEntry{},
+	}
+	for _, datastore := range resp.Msg.Datastores {
+		response.Datastores = append(response.Datastores, DatastoresResponseEntry{
+			Name:     datastore.Name,
+			Type:     datastore.Type.Enum().String(),
+			Metadata: datastore.Metadata,
+			Env:      datastore.Env,
+		})
+	}
+
+	h.WriteResult(w, r, response)
+}

+ 88 - 0
api/server/router/cloud_provider.go

@@ -0,0 +1,88 @@
+package router
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/porter-dev/porter/api/server/handlers/cloud_provider"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+// NewCloudProviderScopedRegisterer returns a scoped route registerer for CloudProvider routes
+func NewCloudProviderScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetCloudProviderScopedRoutes,
+		Children:  children,
+	}
+}
+
+// GetCloudProviderScopedRoutes returns scoped CloudProvider routes with mounted child registerers
+func GetCloudProviderScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getCloudProviderRoutes(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
+}
+
+// getCloudProviderRoutes returns CloudProvider routes
+func getCloudProviderRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/cloud-providers"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+	routes := make([]*router.Route, 0)
+
+	// GET /api/projects/{project_id}/cloud-providers/aws -> cloud_provider.NewListAwsHandler
+	listAwsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/aws", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listAwsHandler := cloud_provider.NewListAwsAccountsHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listAwsEndpoint,
+		Handler:  listAwsHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 118 - 0
api/server/router/datastore.go

@@ -0,0 +1,118 @@
+package router
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/porter-dev/porter/api/server/handlers/datastore"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+// NewDatastoreScopedRegisterer returns a scoped route registerer for Datastore routes
+func NewDatastoreScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetDatastoreScopedRoutes,
+		Children:  children,
+	}
+}
+
+// GetDatastoreScopedRoutes returns scoped Datastore routes with mounted child registerers
+func GetDatastoreScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getDatastoreRoutes(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
+}
+
+// getDatastoreRoutes returns Datastore routes
+func getDatastoreRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	// empty path as this is mounted onto the datastore endpoints
+	relPath := ""
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+	routes := make([]*router.Route, 0)
+
+	// GET /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores -> cloud_provider.NewListHandler
+	listEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/{%s}/datastores", relPath, types.URLParamCloudProviderType, types.URLParamCloudProviderID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listHandler := datastore.NewListDatastoresHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listEndpoint,
+		Handler:  listHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/cloud-providers/{cloud_provider_type}/{cloud_provider_id}/datastores/{datastore_type}/{datastore_name} -> cloud_provider.NewListHandler
+	getEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/{%s}/datastores/{%s}/{%s}", relPath, types.URLParamCloudProviderType, types.URLParamCloudProviderID, types.URLParamDatastoreType, types.URLParamDatastoreName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getHandler := datastore.NewListDatastoresHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getEndpoint,
+		Handler:  getHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 3 - 0
api/server/router/router.go

@@ -30,6 +30,8 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 
 	releaseRegisterer := NewReleaseScopedRegisterer()
 	namespaceRegisterer := NewNamespaceScopedRegisterer(releaseRegisterer)
+	datastoreRegisterer := NewDatastoreScopedRegisterer()
+	cloudProviderRegisterer := NewCloudProviderScopedRegisterer(datastoreRegisterer)
 	clusterIntegrationRegisterer := NewClusterIntegrationScopedRegisterer()
 	stackRegisterer := NewPorterAppScopedRegisterer()
 	deploymentTargetRegisterer := NewDeploymentTargetScopedRegisterer()
@@ -43,6 +45,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	projectOAuthRegisterer := NewProjectOAuthScopedRegisterer()
 	slackIntegrationRegisterer := NewSlackIntegrationScopedRegisterer()
 	projRegisterer := NewProjectScopedRegisterer(
+		cloudProviderRegisterer,
 		clusterRegisterer,
 		registryRegisterer,
 		helmRepoRegisterer,

+ 4 - 0
api/types/request.go

@@ -53,6 +53,10 @@ const (
 	URLParamPorterAppName         URLParam = "porter_app_name"
 	URLParamPorterAppEventID      URLParam = "porter_app_event_id"
 	URLParamAppRevisionID         URLParam = "app_revision_id"
+	URLParamDatastoreType         URLParam = "datastore_type"
+	URLParamDatastoreName         URLParam = "datastore_name"
+	URLParamCloudProviderType     URLParam = "cloud_provider_type"
+	URLParamCloudProviderID       URLParam = "cloud_provider_id"
 	URLParamDeploymentTargetID    URLParam = "deployment_target_id"
 	URLParamWebhookID             URLParam = "webhook_id"
 )