Explorar o código

add onboarding endpoints

Alexander Belanger %!s(int64=4) %!d(string=hai) anos
pai
achega
74a1063e66

+ 52 - 0
api/server/handlers/project/get_onboarding.go

@@ -0,0 +1,52 @@
+package project
+
+import (
+	"errors"
+	"fmt"
+	"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/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type OnboardingGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewOnboardingGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OnboardingGetHandler {
+	return &OnboardingGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *OnboardingGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	// look for onboarding
+	onboarding, err := p.Repo().Onboarding().ReadProjectOnboarding(proj.ID)
+	isNotFound := errors.Is(gorm.ErrRecordNotFound, err)
+
+	if isNotFound {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("project onboarding data not found"),
+			http.StatusNotFound,
+		))
+
+		return
+	} else if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// return onboarding data type
+	p.WriteResult(w, r, onboarding.ToOnboardingType())
+}

+ 77 - 0
api/server/handlers/project/update_onboarding.go

@@ -0,0 +1,77 @@
+package project
+
+import (
+	"errors"
+	"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/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type OnboardingUpdateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewOnboardingUpdateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OnboardingUpdateHandler {
+	return &OnboardingUpdateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *OnboardingUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.UpdateOnboardingRequest{}
+
+	if ok := p.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	// look for onboarding
+	onboarding, err := p.Repo().Onboarding().ReadProjectOnboarding(proj.ID)
+	isNotFound := errors.Is(gorm.ErrRecordNotFound, err)
+
+	if err != nil && !isNotFound {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if isNotFound {
+		onboarding = &models.Onboarding{
+			ProjectID: proj.ID,
+		}
+	}
+
+	onboarding.CurrentStep = request.CurrentStep
+	onboarding.ConnectedSource = request.ConnectedSource
+	onboarding.SkipRegistryConnection = request.SkipRegistryConnection
+	onboarding.SkipResourceProvision = request.SkipResourceProvision
+	onboarding.RegistryConnectionID = request.RegistryConnectionID
+	onboarding.RegistryInfraID = request.RegistryInfraID
+	onboarding.ClusterInfraID = request.ClusterInfraID
+
+	if isNotFound {
+		// if not found, create onboarding struct
+		onboarding, err = p.Repo().Onboarding().CreateProjectOnboarding(onboarding)
+	} else {
+		// otherwise, update the onboarding model
+		onboarding, err = p.Repo().Onboarding().UpdateProjectOnboarding(onboarding)
+	}
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// return onboarding data type
+	p.WriteResult(w, r, onboarding.ToOnboardingType())
+}

+ 56 - 0
api/server/router/project.go

@@ -138,6 +138,62 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/onboarding -> project.NewProjectGetOnboardingHandler
+	getOnboardingEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/onboarding",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getOnboardingHandler := project.NewOnboardingGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getOnboardingEndpoint,
+		Handler:  getOnboardingHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/onboarding -> project.NewProjectGetOnboardingHandler
+	updateOnboardingEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/onboarding",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	updateOnboardingHandler := project.NewOnboardingUpdateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: updateOnboardingEndpoint,
+		Handler:  updateOnboardingHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/usage -> project.NewProjectGetUsageHandler
 	getUsageEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 26 - 0
api/types/project.go

@@ -69,3 +69,29 @@ type GetBillingTokenResponse struct {
 type GetProjectBillingResponse struct {
 	HasBilling bool `json:"has_billing"`
 }
+
+type StepEnum string
+
+const (
+	StepGithub StepEnum = "github"
+	StepTwo    StepEnum = "step_two"
+)
+
+type ConnectedSourceType string
+
+const (
+	ConnectedSourceTypeGithub = "github"
+	ConnectedSourceTypeDocker = "docker"
+)
+
+type OnboardingData struct {
+	CurrentStep            StepEnum            `json:"current_step"`
+	ConnectedSource        ConnectedSourceType `json:"connected_source"`
+	SkipRegistryConnection bool                `json:"skip_registry_connection"`
+	SkipResourceProvision  bool                `json:"skip_resource_provision"`
+	RegistryConnectionID   uint                `json:"registry_connection_id"`
+	RegistryInfraID        uint                `json:"registry_infra_id"`
+	ClusterInfraID         uint                `json:"cluster_infra_id"`
+}
+
+type UpdateOnboardingRequest OnboardingData

+ 1 - 0
cmd/migrate/keyrotate/helpers_test.go

@@ -67,6 +67,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.ClusterResolver{},
 		&models.Infra{},
 		&models.GitActionConfig{},
+		&models.Onboarding{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 33 - 0
internal/models/onboarding.go

@@ -0,0 +1,33 @@
+package models
+
+import (
+	"gorm.io/gorm"
+
+	"github.com/porter-dev/porter/api/types"
+)
+
+type Onboarding struct {
+	gorm.Model
+
+	ProjectID              uint
+	CurrentStep            types.StepEnum
+	ConnectedSource        types.ConnectedSourceType
+	SkipRegistryConnection bool
+	SkipResourceProvision  bool
+	RegistryConnectionID   uint
+	RegistryInfraID        uint
+	ClusterInfraID         uint
+}
+
+// ToOnboardingType generates an external types.OnboardingData to be shared over REST
+func (o *Onboarding) ToOnboardingType() *types.OnboardingData {
+	return &types.OnboardingData{
+		CurrentStep:            o.CurrentStep,
+		ConnectedSource:        o.ConnectedSource,
+		SkipRegistryConnection: o.SkipRegistryConnection,
+		SkipResourceProvision:  o.SkipResourceProvision,
+		RegistryConnectionID:   o.RegistryConnectionID,
+		RegistryInfraID:        o.RegistryInfraID,
+		ClusterInfraID:         o.ClusterInfraID,
+	}
+}

+ 1 - 0
internal/repository/gorm/helpers_test.go

@@ -64,6 +64,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.Infra{},
 		&models.GitActionConfig{},
 		&models.Invite{},
+		&models.Onboarding{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 1 - 0
internal/repository/gorm/migrate.go

@@ -31,6 +31,7 @@ func AutoMigrate(db *gorm.DB) error {
 		&models.SubEvent{},
 		&models.ProjectUsage{},
 		&models.ProjectUsageCache{},
+		&models.Onboarding{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 52 - 0
internal/repository/gorm/onboarding.go

@@ -0,0 +1,52 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ProjectOnboardingRepository implements repository.ProjectOnboardingRepository
+type ProjectOnboardingRepository struct {
+	db *gorm.DB
+}
+
+// NewProjectOnboardingRepository will return errors if canQuery is false
+func NewProjectOnboardingRepository(db *gorm.DB) repository.ProjectOnboardingRepository {
+	return &ProjectOnboardingRepository{db}
+}
+
+// CreateProjectOnboarding creates a new project onboarding limit
+func (repo *ProjectOnboardingRepository) CreateProjectOnboarding(
+	onboarding *models.Onboarding,
+) (*models.Onboarding, error) {
+	if err := repo.db.Create(onboarding).Error; err != nil {
+		return nil, err
+	}
+
+	return onboarding, nil
+}
+
+// ReadProjectOnboarding finds the project onboarding matching a project ID
+func (repo *ProjectOnboardingRepository) ReadProjectOnboarding(
+	projID uint,
+) (*models.Onboarding, error) {
+	res := &models.Onboarding{}
+
+	if err := repo.db.Where("project_id = ?", projID).First(res).Error; err != nil {
+		return nil, err
+	}
+
+	return res, nil
+}
+
+// UpdateProjectOnboarding modifies an existing ProjectOnboarding in the database
+func (repo *ProjectOnboardingRepository) UpdateProjectOnboarding(
+	onboarding *models.Onboarding,
+) (*models.Onboarding, error) {
+	if err := repo.db.Save(onboarding).Error; err != nil {
+		return nil, err
+	}
+
+	return onboarding, nil
+}

+ 6 - 0
internal/repository/gorm/repository.go

@@ -32,6 +32,7 @@ type GormRepository struct {
 	notificationConfig        repository.NotificationConfigRepository
 	event                     repository.EventRepository
 	projectUsage              repository.ProjectUsageRepository
+	onboarding                repository.ProjectOnboardingRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -138,6 +139,10 @@ func (t *GormRepository) ProjectUsage() repository.ProjectUsageRepository {
 	return t.projectUsage
 }
 
+func (t *GormRepository) Onboarding() repository.ProjectOnboardingRepository {
+	return t.onboarding
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(db *gorm.DB, key *[32]byte) repository.Repository {
@@ -168,5 +173,6 @@ func NewRepository(db *gorm.DB, key *[32]byte) repository.Repository {
 		notificationConfig:        NewNotificationConfigRepository(db),
 		event:                     NewEventRepository(db),
 		projectUsage:              NewProjectUsageRepository(db),
+		onboarding:                NewProjectOnboardingRepository(db),
 	}
 }

+ 10 - 0
internal/repository/onboarding.go

@@ -0,0 +1,10 @@
+package repository
+
+import "github.com/porter-dev/porter/internal/models"
+
+// ProjectOnboardingRepository represents the set of queries on the Onboarding model
+type ProjectOnboardingRepository interface {
+	CreateProjectOnboarding(onboarding *models.Onboarding) (*models.Onboarding, error)
+	ReadProjectOnboarding(projID uint) (*models.Onboarding, error)
+	UpdateProjectOnboarding(cache *models.Onboarding) (*models.Onboarding, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -27,4 +27,5 @@ type Repository interface {
 	NotificationConfig() NotificationConfigRepository
 	Event() EventRepository
 	ProjectUsage() ProjectUsageRepository
+	Onboarding() ProjectOnboardingRepository
 }

+ 75 - 0
internal/repository/test/onboarding.go

@@ -0,0 +1,75 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ProjectOnboardingRepository implements repository.ProjectOnboardingRepository
+type ProjectOnboardingRepository struct {
+	canQuery    bool
+	onboardings []*models.Onboarding
+}
+
+// NewProjectOnboardingRepository will return errors if canQuery is false
+func NewProjectOnboardingRepository(canQuery bool) repository.ProjectOnboardingRepository {
+	return &ProjectOnboardingRepository{
+		canQuery,
+		[]*models.Onboarding{},
+	}
+}
+
+// CreateProjectOnboarding creates a new project onboarding limit
+func (repo *ProjectOnboardingRepository) CreateProjectOnboarding(
+	onboarding *models.Onboarding,
+) (*models.Onboarding, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if onboarding == nil {
+		return nil, nil
+	}
+
+	repo.onboardings = append(repo.onboardings, onboarding)
+
+	return onboarding, nil
+}
+
+// CreateProjectOnboarding reads a project onboarding by project id
+func (repo *ProjectOnboardingRepository) ReadProjectOnboarding(
+	projID uint,
+) (*models.Onboarding, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	for _, pu := range repo.onboardings {
+		if pu != nil && pu.ProjectID == projID {
+			return pu, nil
+		}
+	}
+
+	return nil, gorm.ErrRecordNotFound
+}
+
+// UpdateProjectOnboarding modifies an existing ProjectOnboarding in the database
+func (repo *ProjectOnboardingRepository) UpdateProjectOnboarding(
+	onboarding *models.Onboarding,
+) (*models.Onboarding, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(onboarding.ID-1) >= len(repo.onboardings) || repo.onboardings[onboarding.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(onboarding.ID - 1)
+	repo.onboardings[index] = onboarding
+
+	return onboarding, nil
+}

+ 6 - 0
internal/repository/test/repository.go

@@ -31,6 +31,7 @@ type TestRepository struct {
 	notificationConfig        repository.NotificationConfigRepository
 	event                     repository.EventRepository
 	projectUsage              repository.ProjectUsageRepository
+	onboarding                repository.ProjectOnboardingRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -137,6 +138,10 @@ func (t *TestRepository) ProjectUsage() repository.ProjectUsageRepository {
 	return t.projectUsage
 }
 
+func (t *TestRepository) Onboarding() repository.ProjectOnboardingRepository {
+	return t.onboarding
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
@@ -167,5 +172,6 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		notificationConfig:        NewNotificationConfigRepository(canQuery),
 		event:                     NewEventRepository(canQuery),
 		projectUsage:              NewProjectUsageRepository(canQuery),
+		onboarding:                NewProjectOnboardingRepository(canQuery),
 	}
 }