Преглед на файлове

Add basic events tab endpoints (#3055)

* adding initial endpoint for events

* add db migration

* add basic endpoint response

* read responses from database

* updating error handling

* status should be conditional
Stefan McShane преди 3 години
родител
ревизия
0c6fe327e7

+ 63 - 0
api/server/handlers/stacks/get_porter_app_events.go

@@ -0,0 +1,63 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/google/uuid"
+	"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"
+)
+
+type GetPorterAppEventHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetPorterAppEventHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetPorterAppEventHandler {
+	return &GetPorterAppEventHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *GetPorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	eventID, reqErr := requestutils.GetURLParamString(r, types.URLParamStackEventID)
+	if reqErr != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		return
+	}
+
+	eventIDasUUID, err := uuid.Parse(eventID)
+	if err != nil {
+		e := fmt.Errorf("unable to parse porter app event id as uuid: %w", err)
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	if eventIDasUUID == uuid.Nil {
+		e := fmt.Errorf("invalid UUID passed for porter app event id")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	event, err := p.Repo().PorterAppEvent().EventByID(eventIDasUUID)
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if event.ID == uuid.Nil {
+		e := fmt.Errorf("porter app event not found")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusNotFound))
+		return
+	}
+
+	p.WriteResult(w, r, event.ToPorterAppEvent())
+}

+ 68 - 0
api/server/handlers/stacks/list_porter_app_events.go

@@ -0,0 +1,68 @@
+package stacks
+
+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/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type PorterAppEventListHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewPorterAppEventListHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *PorterAppEventListHandler {
+	return &PorterAppEventListHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	if reqErr != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		return
+	}
+
+	app, err := p.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	porterApps, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(app.ID)
+	if err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+			return
+		}
+	}
+
+	res := struct {
+		Events []types.PorterAppEvent `json:"events"`
+	}{}
+	res.Events = make([]types.PorterAppEvent, 0)
+
+	for _, porterApp := range porterApps {
+		if porterApp == nil {
+			continue
+		}
+		pa := porterApp.ToPorterAppEvent()
+		pa.Metadata = nil
+		res.Events = append(res.Events, pa)
+	}
+	p.WriteResult(w, r, res)
+}

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

@@ -198,6 +198,62 @@ func getStackRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> stacks.NewPorterAppEventListHandler
+	listPorterAppEventsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/events", relPath, types.URLParamStackName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	listPorterAppEventsHandler := stacks.NewPorterAppEventListHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listPorterAppEventsEndpoint,
+		Handler:  listPorterAppEventsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events/{stack_event_id} -> stacks.NewPorterAppEventGetHandler
+	getPorterAppEventEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/events/{%s}", relPath, types.URLParamStackName, types.URLParamStackEventID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getPorterAppEventHandler := stacks.NewGetPorterAppEventHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getPorterAppEventEndpoint,
+		Handler:  getPorterAppEventHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/analytics -> stacks.NewPorterAppAnalyticsHandler
 	porterAppAnalyticsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 32 - 0
api/types/porter_app.go

@@ -1,5 +1,7 @@
 package types
 
+import "time"
+
 type PorterApp struct {
 	ID        uint `json:"id"`
 	ProjectID uint `json:"project_id"`
@@ -57,3 +59,33 @@ type UpdatePorterAppRequest struct {
 }
 
 type ListPorterAppResponse []*PorterApp
+
+// PorterAppEvent represents an event that occurs on a Porter stack during a stacks lifecycle.
+type PorterAppEvent struct {
+	ID string `json:"id"`
+	// Status contains the accepted status' of a given event such as SUCCESS, FAILED, PROGRESSING, etc.
+	Status string `json:"status,omitempty"`
+	// Type represents a supported Porter Stack Event
+	Type PorterAppEventType `json:"type"`
+	// TypeExternalSource represents an external event source such as Github, or Gitlab. This is not always required but will commonly be see in build events
+	TypeExternalSource string `json:"type_source,omitempty"`
+	// CreatedAt is the time (UTC) that a given event was created. This should not change
+	CreatedAt time.Time `json:"created_at"`
+	// UpdatedAt is the time (UTC) that an event was last updated. This can occur when an event was created as PROGRESSING, then was marked as SUCCESSFUL for example
+	UpdatedAt time.Time `json:"updated_at"`
+	// PorterAppID is the ID that the given event relates to
+	PorterAppID string         `json:"porter_app_id"`
+	Metadata    map[string]any `json:"metadata,omitempty"`
+}
+
+// PorterAppEventType is an alias for a string that represents a Porter Stack Event Type
+type PorterAppEventType string
+
+const (
+	// PorterAppEventType_Build represents a Porter Stack Build event such as in Github or Gitlab
+	PorterAppEventType_Build PorterAppEventType = "BUILD"
+	// PorterAppEventType_Deploy represents a Porter Stack Deploy event which occurred through the Porter UI or CLI
+	PorterAppEventType_Deploy PorterAppEventType = "DEPLOY"
+	// PorterAppEventType_AppEvent represents a Porter Stack App Event which occurred whilst the application was running, such as an OutOfMemory (OOM) error
+	PorterAppEventType_AppEvent PorterAppEventType = "APP_EVENT"
+)

+ 1 - 0
api/types/request.go

@@ -50,6 +50,7 @@ const (
 	URLParamIntegrationID         URLParam = "integration_id"
 	URLParamAPIContractRevisionID URLParam = "contract_revision_id"
 	URLParamStackName             URLParam = "stack_name"
+	URLParamStackEventID          URLParam = "stack_event_id"
 )
 
 type Path struct {

+ 52 - 0
internal/models/porter_app_event.go

@@ -0,0 +1,52 @@
+package models
+
+import (
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+// PorterAppEvent represents an event that occurs on a Porter stack during a stacks lifecycle.
+type PorterAppEvent struct {
+	gorm.Model
+
+	// ID is a unique identifier for a given event
+	ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
+	// Status contains the accepted status' of a given event such as SUCCESS, FAILED, PROGRESSING, etc.
+	Status string `json:"status"`
+	// Type represents a supported Porter Stack Event
+	Type string `json:"type"`
+	// TypeExternalSource represents an external event source such as Github, or Gitlab. This is not always required but will commonly be see in build events
+	TypeExternalSource string `json:"type_source,omitempty"`
+	// CreatedAt is the time (UTC) that a given event was created. This should not change
+	CreatedAt time.Time `json:"created_at"`
+	// UpdatedAt is the time (UTC) that an event was last updated. This can occur when an event was created as PROGRESSING, then was marked as SUCCESSFUL for example
+	UpdatedAt time.Time `json:"updated_at"`
+	// PorterAppID is the ID that the given event relates to
+	PorterAppID string `json:"porter_app_id"`
+	Metadata    JSONB  `json:"metadata" sql:"type:jsonb" gorm:"type:jsonb"`
+}
+
+// TableName overrides the table name
+func (PorterAppEvent) TableName() string {
+	return "porter_app_events"
+}
+
+func (p *PorterAppEvent) ToPorterAppEvent() types.PorterAppEvent {
+	if p == nil {
+		return types.PorterAppEvent{}
+	}
+	ty := types.PorterAppEvent{
+		ID:                 p.ID.String(),
+		Status:             p.Status,
+		Type:               types.PorterAppEventType(p.Type),
+		TypeExternalSource: p.TypeExternalSource,
+		CreatedAt:          p.CreatedAt,
+		UpdatedAt:          p.UpdatedAt,
+		PorterAppID:        p.PorterAppID,
+		Metadata:           p.Metadata,
+	}
+	return ty
+}

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

@@ -61,6 +61,7 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&models.APIContractRevision{},
 		&models.AWSAssumeRoleChain{},
 		&models.PorterApp{},
+		&models.PorterAppEvent{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 56 - 0
internal/repository/gorm/porter_app_event.go

@@ -0,0 +1,56 @@
+package gorm
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+
+	"github.com/google/uuid"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// PorterAppEventRepository uses gorm.DB for querying the database
+type PorterAppEventRepository struct {
+	db *gorm.DB
+}
+
+// NewPorterAppEventRepository returns a PorterAppEventRepository which uses
+// gorm.DB for querying the database
+func NewPorterAppEventRepository(db *gorm.DB) repository.PorterAppEventRepository {
+	return &PorterAppEventRepository{db}
+}
+
+func (repo *PorterAppEventRepository) ListEventsByPorterAppID(porterAppID uint) ([]*models.PorterAppEvent, error) {
+	apps := []*models.PorterAppEvent{}
+
+	id := strconv.Itoa(int(porterAppID))
+	if id == "" {
+		return nil, errors.New("invalid porter app id supplied")
+	}
+
+	if err := repo.db.Where("porter_app_id = ?", id).Find(&apps).Error; err != nil {
+		fmt.Println("STEFAN", err)
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, err
+		}
+	}
+
+	return apps, nil
+}
+
+func (repo *PorterAppEventRepository) EventByID(eventID uuid.UUID) (*models.PorterAppEvent, error) {
+	app := &models.PorterAppEvent{}
+
+	if eventID == uuid.Nil {
+		return app, errors.New("invalid porter app event id supplied")
+	}
+
+	tx := repo.db.Find(&app, "id = ?", eventID.String())
+	if tx.Error != nil {
+		return app, fmt.Errorf("no porter app event found for id %s: %w", eventID, tx.Error)
+	}
+
+	return app, nil
+}

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

@@ -52,6 +52,7 @@ type GormRepository struct {
 	apiContractRevisions      repository.APIContractRevisioner
 	awsAssumeRoleChainer      repository.AWSAssumeRoleChainer
 	porterApp                 repository.PorterAppRepository
+	porterAppEvent            repository.PorterAppEventRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -234,6 +235,10 @@ func (t *GormRepository) AWSAssumeRoleChainer() repository.AWSAssumeRoleChainer
 	return t.awsAssumeRoleChainer
 }
 
+func (t *GormRepository) PorterAppEvent() repository.PorterAppEventRepository {
+	return t.porterAppEvent
+}
+
 // 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, storageBackend credentials.CredentialStorage) repository.Repository {
@@ -283,5 +288,6 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		apiContractRevisions:      NewAPIContractRevisioner(db),
 		awsAssumeRoleChainer:      NewAWSAssumeRoleChainer(db),
 		porterApp:                 NewPorterAppRepository(db),
+		porterAppEvent:            NewPorterAppEventRepository(db),
 	}
 }

+ 12 - 0
internal/repository/porter_app_event.go

@@ -0,0 +1,12 @@
+package repository
+
+import (
+	"github.com/google/uuid"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// PorterAppEventRepository represents the set of queries on the PorterAppEvent model
+type PorterAppEventRepository interface {
+	ListEventsByPorterAppID(porterAppID uint) ([]*models.PorterAppEvent, error)
+	EventByID(eventID uuid.UUID) (*models.PorterAppEvent, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -46,4 +46,5 @@ type Repository interface {
 	APIContractRevisioner() APIContractRevisioner
 	AWSAssumeRoleChainer() AWSAssumeRoleChainer
 	PorterApp() PorterAppRepository
+	PorterAppEvent() PorterAppEventRepository
 }

+ 0 - 1
internal/repository/test/porter_app.go

@@ -15,7 +15,6 @@ type PorterAppRepository struct {
 
 func NewPorterAppRepository(canQuery bool, failingMethods ...string) repository.PorterAppRepository {
 	return &PorterAppRepository{canQuery, strings.Join(failingMethods, ",")}
-
 }
 
 func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {

+ 25 - 0
internal/repository/test/porter_app_event.go

@@ -0,0 +1,25 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/google/uuid"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type PorterAppEventRepository struct {
+	canQuery bool
+}
+
+func NewPorterAppEventRepository(canQuery bool, failingMethods ...string) repository.PorterAppEventRepository {
+	return &PorterAppEventRepository{canQuery: false}
+}
+
+func (repo *PorterAppEventRepository) ListEventsByPorterAppID(porterAppID uint) ([]*models.PorterAppEvent, error) {
+	return nil, errors.New("cannot write database")
+}
+
+func (repo *PorterAppEventRepository) EventByID(eventID uuid.UUID) (*models.PorterAppEvent, error) {
+	return &models.PorterAppEvent{}, errors.New("cannot write database")
+}

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

@@ -50,6 +50,7 @@ type TestRepository struct {
 	apiContractRevision       repository.APIContractRevisioner
 	awsAssumeRoleChainer      repository.AWSAssumeRoleChainer
 	porterApp                 repository.PorterAppRepository
+	porterAppEvent            repository.PorterAppEventRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -232,6 +233,10 @@ func (t *TestRepository) PorterApp() repository.PorterAppRepository {
 	return t.porterApp
 }
 
+func (t *TestRepository) PorterAppEvent() repository.PorterAppEventRepository {
+	return t.porterAppEvent
+}
+
 // 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 {
@@ -281,5 +286,6 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		apiContractRevision:       NewAPIContractRevisioner(),
 		awsAssumeRoleChainer:      NewAWSAssumeRoleChainer(),
 		porterApp:                 NewPorterAppRepository(canQuery, failingMethods...),
+		porterAppEvent:            NewPorterAppEventRepository(canQuery),
 	}
 }