Kaynağa Gözat

initial version of porter agent backend impl

Alexander Belanger 4 yıl önce
ebeveyn
işleme
413434d49c

+ 51 - 0
api/server/handlers/cluster/detect_agent_installed.go

@@ -0,0 +1,51 @@
+package cluster
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"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/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type DetectAgentInstalledHandler struct {
+	handlers.PorterHandler
+	authz.KubernetesAgentGetter
+}
+
+func NewDetectAgentInstalledHandler(
+	config *config.Config,
+) *DetectAgentInstalledHandler {
+	return &DetectAgentInstalledHandler{
+		PorterHandler:         handlers.NewDefaultPorterHandler(config, nil, nil),
+		KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *DetectAgentInstalledHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	_, err = agent.GetPorterAgent()
+
+	if targetErr := kubernetes.IsNotFoundError; err != nil && errors.Is(err, targetErr) {
+		http.NotFound(w, r)
+		return
+	} else if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}

+ 111 - 0
api/server/handlers/cluster/install_agent.go

@@ -0,0 +1,111 @@
+package cluster
+
+import (
+	"fmt"
+	"net/http"
+
+	"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/auth/token"
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type InstallAgentHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewInstallAgentHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *InstallAgentHandler {
+	return &InstallAgentHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "porter-agent-system")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	chart, err := loader.LoadChartPublic(c.Config().ServerConf.DefaultAddonHelmRepoURL, "porter-agent", "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// create namespace if not exists
+	_, err = helmAgent.K8sAgent.CreateNamespace("porter-agent-system")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// add api token to values
+	jwt, err := token.GetTokenForAPI(user.ID, proj.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	porterAgentValues := map[string]interface{}{
+		"agent": map[string]interface{}{
+			"image":       "public.ecr.aws/o1j4x7p4/porter-agent:latest",
+			"porterHost":  c.Config().ServerConf.ServerURL,
+			"porterPort":  "443",
+			"porterToken": encoded,
+			"privateRegistry": map[string]interface{}{
+				"enabled": false,
+			},
+			"clusterID": fmt.Sprintf("%d", cluster.ID),
+			"projectID": fmt.Sprintf("%d", proj.ID),
+		},
+	}
+
+	conf := &helm.InstallChartConfig{
+		Chart:     chart,
+		Name:      "porter-agent",
+		Namespace: "porter-agent-system",
+		Cluster:   cluster,
+		Repo:      c.Repo(),
+		Values:    porterAgentValues,
+	}
+
+	_, err = helmAgent.InstallChart(conf, c.Config().DOConf)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error installing a new chart: %s", err.Error()),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}

+ 1 - 1
api/server/handlers/release/get_steps.go

@@ -50,7 +50,7 @@ func (c *GetReleaseStepsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	res := make(types.GetReleaseStepsResponse, 0)
 
 	if release.EventContainer != 0 {
-		subevents, err := c.Repo().Event().ReadEventsByContainerID(release.EventContainer)
+		subevents, err := c.Repo().BuildEvent().ReadEventsByContainerID(release.EventContainer)
 
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 3 - 3
api/server/handlers/release/update_steps.go

@@ -55,7 +55,7 @@ func (c *UpdateReleaseStepsHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 
 	if release.EventContainer == 0 {
 		// create new event container
-		container, err := c.Repo().Event().CreateEventContainer(&models.EventContainer{ReleaseID: release.ID})
+		container, err := c.Repo().BuildEvent().CreateEventContainer(&models.EventContainer{ReleaseID: release.ID})
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
@@ -72,14 +72,14 @@ func (c *UpdateReleaseStepsHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 
 	}
 
-	container, err := c.Repo().Event().ReadEventContainer(release.EventContainer)
+	container, err := c.Repo().BuildEvent().ReadEventContainer(release.EventContainer)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	if err := c.Repo().Event().AppendEvent(container, &models.SubEvent{
+	if err := c.Repo().BuildEvent().AppendEvent(container, &models.SubEvent{
 		EventContainerID: container.ID,
 		EventID:          request.Event.EventID,
 		Name:             request.Event.Name,

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

@@ -451,6 +451,60 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/agent/detect -> cluster.NewDetectAgentInstalledHandler
+	detectAgentInstalledEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/agent/detect",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	detectAgentInstalledHandler := cluster.NewDetectAgentInstalledHandler(config)
+
+	routes = append(routes, &Route{
+		Endpoint: detectAgentInstalledEndpoint,
+		Handler:  detectAgentInstalledHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/agent/install -> cluster.NewInstallAgentHandler
+	installAgentEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/agent/install",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	installAgentHandler := cluster.NewInstallAgentHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: installAgentEndpoint,
+		Handler:  installAgentHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/prometheus/ingresses -> cluster.NewListNGINXIngressesHandler
 	listNGINXIngressesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 51 - 0
api/types/kube_events.go

@@ -0,0 +1,51 @@
+package types
+
+import "time"
+
+// CreateKubeEventRequest is the type for creating a new kube event
+type CreateKubeEventRequest struct {
+	ResourceType string    `json:"resource_type"`
+	Name         string    `json:"name"`
+	OwnerType    string    `json:"owner_type"`
+	OwnerName    string    `json:"owner_name"`
+	EventType    string    `json:"event_type"`
+	Namespace    string    `json:"namespace"`
+	Message      string    `json:"message"`
+	Reason       string    `json:"reason"`
+	Timestamp    time.Time `json:"timestamp"`
+	Data         []string  `json:"data"`
+}
+
+type KubeEventBasic struct {
+	ID        uint
+	ProjectID uint
+	ClusterID uint
+
+	ResourceType string    `json:"resource_type"`
+	Name         string    `json:"name"`
+	OwnerType    string    `json:"owner_type"`
+	OwnerName    string    `json:"owner_name"`
+	EventType    string    `json:"event_type"`
+	Namespace    string    `json:"namespace"`
+	Message      string    `json:"message"`
+	Reason       string    `json:"reason"`
+	Timestamp    time.Time `json:"timestamp"`
+}
+
+type KubeEvent struct {
+	*KubeEventBasic
+
+	Data []byte `json:"data"`
+}
+
+type ListKubeEventRequest struct {
+	Limit int    `schema:"limit"`
+	Skip  int    `schema:"skip"`
+	Type  string `schema:"type"`
+
+	// can only be "timestamp" for now
+	SortBy string `schema:"sort_by"`
+
+	OwnerType string `schema:"owner_type"`
+	OwnerName string `schema:"owner_name"`
+}

+ 25 - 0
internal/kubernetes/agent.go

@@ -267,6 +267,17 @@ func (a *Agent) ListNamespaces() (*v1.NamespaceList, error) {
 
 // CreateNamespace creates a namespace with the given name.
 func (a *Agent) CreateNamespace(name string) (*v1.Namespace, error) {
+	// check if namespace exists
+	checkNS, err := a.Clientset.CoreV1().Namespaces().Get(
+		context.TODO(),
+		name,
+		metav1.GetOptions{},
+	)
+
+	if err == nil && checkNS != nil {
+		return checkNS, nil
+	}
+
 	namespace := v1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: name,
@@ -289,6 +300,20 @@ func (a *Agent) DeleteNamespace(name string) error {
 	)
 }
 
+func (a *Agent) GetPorterAgent() (*appsv1.Deployment, error) {
+	depl, err := a.Clientset.AppsV1().Deployments("porter-agent-system").Get(
+		context.TODO(),
+		"porter-agent-controller-manager",
+		metav1.GetOptions{},
+	)
+
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	}
+
+	return depl, err
+}
+
 // ListJobsByLabel lists jobs in a namespace matching a label
 type Label struct {
 	Key string

+ 52 - 0
internal/models/kube_events.go

@@ -0,0 +1,52 @@
+package models
+
+import (
+	"time"
+
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+// KubeEvent model refers to a type of event from a Kubernetes cluster
+type KubeEvent struct {
+	gorm.Model
+
+	ProjectID uint
+	ClusterID uint
+
+	ResourceType string
+	Name         string
+	OwnerType    string
+	OwnerName    string
+	EventType    string
+	Namespace    string
+	Message      string
+	Reason       string
+	Timestamp    time.Time
+	Data         []byte
+}
+
+// ToKubeEventType generates an external KubeEvent to be shared over REST
+func (k *KubeEvent) ToKubeEventType() *types.KubeEvent {
+	return &types.KubeEvent{
+		KubeEventBasic: k.ToKubeEventBasicType(),
+		Data:           k.Data,
+	}
+}
+
+func (k *KubeEvent) ToKubeEventBasicType() *types.KubeEventBasic {
+	return &types.KubeEventBasic{
+		ID:           k.ID,
+		ProjectID:    k.ProjectID,
+		ClusterID:    k.ClusterID,
+		ResourceType: k.ResourceType,
+		Name:         k.Name,
+		Namespace:    k.Namespace,
+		OwnerType:    k.OwnerType,
+		OwnerName:    k.OwnerName,
+		EventType:    k.EventType,
+		Message:      k.Message,
+		Reason:       k.Reason,
+		Timestamp:    k.Timestamp,
+	}
+}

+ 17 - 2
internal/repository/event.go

@@ -1,8 +1,11 @@
 package repository
 
-import "github.com/porter-dev/porter/internal/models"
+import (
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
 
-type EventRepository interface {
+type BuildEventRepository interface {
 	CreateEventContainer(am *models.EventContainer) (*models.EventContainer, error)
 	CreateSubEvent(am *models.SubEvent) (*models.SubEvent, error)
 	ReadEventsByContainerID(id uint) ([]*models.SubEvent, error)
@@ -10,3 +13,15 @@ type EventRepository interface {
 	ReadSubEvent(id uint) (*models.SubEvent, error)
 	AppendEvent(container *models.EventContainer, event *models.SubEvent) error
 }
+
+type KubeEventRepository interface {
+	CreateEvent(event *models.KubeEvent) (*models.KubeEvent, error)
+	ReadEvent(id uint, projID uint, clusterID uint) (*models.KubeEvent, error)
+	ListEventsByProjectID(
+		projectID uint,
+		clusterID uint,
+		opts *types.ListKubeEventRequest,
+		shouldDecrypt bool,
+	) ([]*models.KubeEvent, error)
+	DeleteEvent(id uint) error
+}

+ 170 - 11
internal/repository/gorm/event.go

@@ -1,37 +1,40 @@
 package gorm
 
 import (
+	"strings"
+
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 )
 
-// EventRepository holds both EventContainer and SubEvent models
-type EventRepository struct {
+// BuildEventRepository holds both EventContainer and SubEvent models
+type BuildEventRepository struct {
 	db *gorm.DB
 }
 
-// NewEventRepository returns a EventRepository which uses
+// NewBuildEventRepository returns a BuildEventRepository which uses
 // gorm.DB for querying the database
-func NewEventRepository(db *gorm.DB) repository.EventRepository {
-	return &EventRepository{db}
+func NewBuildEventRepository(db *gorm.DB) repository.BuildEventRepository {
+	return &BuildEventRepository{db}
 }
 
-func (repo EventRepository) CreateEventContainer(am *models.EventContainer) (*models.EventContainer, error) {
+func (repo BuildEventRepository) CreateEventContainer(am *models.EventContainer) (*models.EventContainer, error) {
 	if err := repo.db.Create(am).Error; err != nil {
 		return nil, err
 	}
 	return am, nil
 }
 
-func (repo EventRepository) CreateSubEvent(am *models.SubEvent) (*models.SubEvent, error) {
+func (repo BuildEventRepository) CreateSubEvent(am *models.SubEvent) (*models.SubEvent, error) {
 	if err := repo.db.Create(am).Error; err != nil {
 		return nil, err
 	}
 	return am, nil
 }
 
-func (repo EventRepository) ReadEventsByContainerID(id uint) ([]*models.SubEvent, error) {
+func (repo BuildEventRepository) ReadEventsByContainerID(id uint) ([]*models.SubEvent, error) {
 	var events []*models.SubEvent
 	if err := repo.db.Where("event_container_id = ?", id).Find(&events).Error; err != nil {
 		return nil, err
@@ -39,7 +42,7 @@ func (repo EventRepository) ReadEventsByContainerID(id uint) ([]*models.SubEvent
 	return events, nil
 }
 
-func (repo EventRepository) ReadEventContainer(id uint) (*models.EventContainer, error) {
+func (repo BuildEventRepository) ReadEventContainer(id uint) (*models.EventContainer, error) {
 	container := &models.EventContainer{}
 	if err := repo.db.Where("id = ?", id).First(&container).Error; err != nil {
 		return nil, err
@@ -47,7 +50,7 @@ func (repo EventRepository) ReadEventContainer(id uint) (*models.EventContainer,
 	return container, nil
 }
 
-func (repo EventRepository) ReadSubEvent(id uint) (*models.SubEvent, error) {
+func (repo BuildEventRepository) ReadSubEvent(id uint) (*models.SubEvent, error) {
 	event := &models.SubEvent{}
 	if err := repo.db.Where("id = ?", id).First(&event).Error; err != nil {
 		return nil, err
@@ -57,7 +60,163 @@ func (repo EventRepository) ReadSubEvent(id uint) (*models.SubEvent, error) {
 
 // AppendEvent will check if subevent with same (id, index) already exists
 // if yes, overrite it, otherwise make a new subevent
-func (repo EventRepository) AppendEvent(container *models.EventContainer, event *models.SubEvent) error {
+func (repo BuildEventRepository) AppendEvent(container *models.EventContainer, event *models.SubEvent) error {
 	event.EventContainerID = container.ID
 	return repo.db.Create(event).Error
 }
+
+// KubeEventRepository uses gorm.DB for querying the database
+type KubeEventRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewKubeEventRepository returns an KubeEventRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewKubeEventRepository(db *gorm.DB, key *[32]byte) repository.KubeEventRepository {
+	return &KubeEventRepository{db, key}
+}
+
+// CreateEvent creates a new kube auth mechanism
+func (repo *KubeEventRepository) CreateEvent(
+	event *models.KubeEvent,
+) (*models.KubeEvent, error) {
+	err := repo.EncryptKubeEventData(event, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if err := repo.db.Create(event).Error; err != nil {
+		return nil, err
+	}
+
+	return event, nil
+}
+
+// ReadEvent finds an event by id
+func (repo *KubeEventRepository) ReadEvent(
+	id, projID, clusterID uint,
+) (*models.KubeEvent, error) {
+	event := &models.KubeEvent{}
+
+	// preload Clusters association
+	if err := repo.db.Where(
+		"id = ? AND project_id = ? AND cluster_id = ?",
+		id,
+		projID,
+		clusterID,
+	).First(&event).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptKubeEventData(event, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return event, nil
+}
+
+// ListEventsByProjectID finds all events for a given project id
+// with the given options
+func (repo *KubeEventRepository) ListEventsByProjectID(
+	projectID uint,
+	clusterID uint,
+	opts *types.ListKubeEventRequest,
+	shouldDecrypt bool,
+) ([]*models.KubeEvent, error) {
+	listOpts := opts
+
+	if listOpts.Limit == 0 {
+		listOpts.Limit = 50
+	}
+
+	events := []*models.KubeEvent{}
+
+	query := repo.db.Where("project_id = ? AND cluster_id = ?", projectID, clusterID)
+
+	if listOpts.Type != "" {
+		query = query.Where(
+			"event_type = ?",
+			strings.ToLower(listOpts.Type),
+		)
+	}
+
+	if listOpts.OwnerName != "" && listOpts.OwnerType != "" {
+		query = query.Where(
+			"owner_name = ? AND owner_type = ?",
+			listOpts.OwnerName,
+			listOpts.OwnerType,
+		)
+	}
+
+	query = query.Limit(listOpts.Limit).Offset(listOpts.Skip)
+
+	if listOpts.SortBy == "timestamp" {
+		query = query.Order("timestamp desc").Order("id desc")
+	}
+
+	if err := query.Find(&events).Error; err != nil {
+		return nil, err
+	}
+
+	if shouldDecrypt {
+		for _, event := range events {
+			repo.DecryptKubeEventData(event, repo.key)
+		}
+	}
+
+	return events, nil
+}
+
+// DeleteEvent deletes an event by ID
+func (repo *KubeEventRepository) DeleteEvent(
+	id uint,
+) error {
+	if err := repo.db.Where("id = ?", id).Delete(&models.KubeEvent{}).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// EncryptEventData will encrypt the event data before
+// writing to the DB
+func (repo *KubeEventRepository) EncryptKubeEventData(
+	event *models.KubeEvent,
+	key *[32]byte,
+) error {
+	if len(event.Data) > 0 {
+		cipherData, err := repository.Encrypt(event.Data, key)
+
+		if err != nil {
+			return err
+		}
+
+		event.Data = cipherData
+	}
+
+	return nil
+}
+
+// DecryptEventData will decrypt the event data before
+// returning it from the DB
+func (repo *KubeEventRepository) DecryptKubeEventData(
+	event *models.KubeEvent,
+	key *[32]byte,
+) error {
+	if len(event.Data) > 0 {
+		plaintext, err := repository.Decrypt(event.Data, key)
+
+		if err != nil {
+			return err
+		}
+
+		event.Data = plaintext
+	}
+
+	return nil
+}

+ 148 - 0
internal/repository/gorm/event_test.go

@@ -0,0 +1,148 @@
+package gorm_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+func TestCreateKubeEvent(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_event.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initCluster(tester, t)
+	defer cleanup(tester, t)
+
+	event := &models.KubeEvent{
+		ProjectID: tester.initProjects[0].Model.ID,
+		ClusterID: tester.initClusters[0].Model.ID,
+		EventType: "pod",
+		Name:      "pod-example-1",
+		Namespace: "default",
+		Message:   "Pod killed",
+		Reason:    "OOM: memory limit exceeded",
+		Data:      []byte("log from pod\nlog2 from pod"),
+	}
+
+	copyKubeEvent := *event
+
+	event, err := tester.repo.KubeEvent().CreateEvent(event)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	event, err = tester.repo.KubeEvent().ReadEvent(event.Model.ID, 1, 1)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1 and name is "ecr"
+	if event.Model.ID != 1 {
+		t.Errorf("incorrect registry ID: expected %d, got %d\n", 1, event.Model.ID)
+	}
+
+	event.Model = gorm.Model{}
+
+	if diff := deep.Equal(event, &copyKubeEvent); diff != nil {
+		t.Errorf("tokens not equal:")
+		t.Error(diff)
+	}
+}
+
+func TestListKubeEventsByProjectIDWithLimit(t *testing.T) {
+	suffix, _ := repository.GenerateRandomBytes(4)
+
+	tester := &tester{
+		dbFileName: fmt.Sprintf("./porter_list_events_%s.db", suffix),
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initCluster(tester, t)
+	initKubeEvents(tester, t)
+	defer cleanup(tester, t)
+
+	testListKubeEventsByProjectID(tester, t, 1, true, &types.ListKubeEventRequest{
+		Limit: 10,
+		Type:  "node",
+	}, tester.initKubeEvents[50:60])
+}
+
+func TestListKubeEventsByProjectIDWithSkip(t *testing.T) {
+	suffix, _ := repository.GenerateRandomBytes(4)
+
+	tester := &tester{
+		dbFileName: fmt.Sprintf("./porter_list_events_%s.db", suffix),
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initCluster(tester, t)
+	initKubeEvents(tester, t)
+	defer cleanup(tester, t)
+
+	testListKubeEventsByProjectID(tester, t, 1, true, &types.ListKubeEventRequest{
+		Limit: 25,
+		Skip:  10,
+	}, tester.initKubeEvents[10:35])
+}
+
+func TestListKubeEventsByProjectIDWithSortBy(t *testing.T) {
+	suffix, _ := repository.GenerateRandomBytes(4)
+
+	tester := &tester{
+		dbFileName: fmt.Sprintf("./porter_list_events_%s.db", suffix),
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initCluster(tester, t)
+	initKubeEvents(tester, t)
+	defer cleanup(tester, t)
+
+	testListKubeEventsByProjectID(tester, t, 1, true, &types.ListKubeEventRequest{
+		Limit:  1,
+		Skip:   0,
+		Type:   "node",
+		SortBy: "timestamp",
+	}, tester.initKubeEvents[99:])
+}
+
+func testListKubeEventsByProjectID(tester *tester, t *testing.T, clusterID uint, decrypt bool, opts *types.ListKubeEventRequest, expKubeEvents []*models.KubeEvent) {
+	t.Helper()
+
+	events, err := tester.repo.KubeEvent().ListEventsByProjectID(
+		tester.initProjects[0].Model.ID,
+		clusterID,
+		opts,
+		decrypt,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure data is correct
+	if len(events) != len(expKubeEvents) {
+		t.Fatalf("length of events incorrect: expected %d, got %d\n", len(expKubeEvents), len(events))
+	}
+
+	for _, expKubeEvent := range expKubeEvents {
+		expKubeEvent.Data = []byte("log from pod\nlog2 from pod")
+	}
+
+	if diff := deep.Equal(expKubeEvents, events); diff != nil {
+		t.Errorf("incorrect events")
+		t.Error(diff)
+	}
+}

+ 62 - 19
internal/repository/gorm/helpers_test.go

@@ -1,6 +1,7 @@
 package gorm_test
 
 import (
+	"fmt"
 	"os"
 	"testing"
 	"time"
@@ -15,25 +16,26 @@ import (
 )
 
 type tester struct {
-	repo         repository.Repository
-	key          *[32]byte
-	dbFileName   string
-	initUsers    []*models.User
-	initProjects []*models.Project
-	initGRs      []*models.GitRepo
-	initRegs     []*models.Registry
-	initClusters []*models.Cluster
-	initHRs      []*models.HelmRepo
-	initInfras   []*models.Infra
-	initReleases []*models.Release
-	initInvites  []*models.Invite
-	initCCs      []*models.ClusterCandidate
-	initKIs      []*ints.KubeIntegration
-	initBasics   []*ints.BasicIntegration
-	initOIDCs    []*ints.OIDCIntegration
-	initOAuths   []*ints.OAuthIntegration
-	initGCPs     []*ints.GCPIntegration
-	initAWSs     []*ints.AWSIntegration
+	repo           repository.Repository
+	key            *[32]byte
+	dbFileName     string
+	initUsers      []*models.User
+	initProjects   []*models.Project
+	initGRs        []*models.GitRepo
+	initRegs       []*models.Registry
+	initClusters   []*models.Cluster
+	initHRs        []*models.HelmRepo
+	initInfras     []*models.Infra
+	initReleases   []*models.Release
+	initInvites    []*models.Invite
+	initKubeEvents []*models.KubeEvent
+	initCCs        []*models.ClusterCandidate
+	initKIs        []*ints.KubeIntegration
+	initBasics     []*ints.BasicIntegration
+	initOIDCs      []*ints.OIDCIntegration
+	initOAuths     []*ints.OAuthIntegration
+	initGCPs       []*ints.GCPIntegration
+	initAWSs       []*ints.AWSIntegration
 }
 
 func setupTestEnv(tester *tester, t *testing.T) {
@@ -64,6 +66,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.Infra{},
 		&models.GitActionConfig{},
 		&models.Invite{},
+		&models.KubeEvent{},
 		&models.Onboarding{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
@@ -548,3 +551,43 @@ func initRelease(tester *tester, t *testing.T) {
 
 	tester.initReleases = append(tester.initReleases, release)
 }
+
+func initKubeEvents(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	initEvents := make([]*models.KubeEvent, 0)
+
+	// init 100 events for testing purposes
+	for i := 0; i < 100; i++ {
+		refType := "pod"
+
+		if i >= 50 {
+			refType = "node"
+		}
+
+		event := &models.KubeEvent{
+			ProjectID: tester.initProjects[0].Model.ID,
+			ClusterID: tester.initClusters[0].Model.ID,
+			EventType: refType,
+			Name:      fmt.Sprintf("%s-example-%d", refType, i),
+			Namespace: "default",
+			Message:   "Pod killed",
+			Reason:    "OOM: memory limit exceeded",
+			Data:      []byte("log from pod\nlog2 from pod"),
+		}
+
+		event, err := tester.repo.KubeEvent().CreateEvent(event)
+
+		if err != nil {
+			t.Fatalf("%v\n", err)
+		}
+
+		initEvents = append(initEvents, event)
+	}
+
+	tester.initKubeEvents = initEvents
+}

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

@@ -29,6 +29,7 @@ func AutoMigrate(db *gorm.DB) error {
 		&models.NotificationConfig{},
 		&models.EventContainer{},
 		&models.SubEvent{},
+		&models.KubeEvent{},
 		&models.ProjectUsage{},
 		&models.ProjectUsageCache{},
 		&models.Onboarding{},

+ 10 - 4
internal/repository/gorm/repository.go

@@ -31,7 +31,8 @@ type GormRepository struct {
 	githubAppOAuthIntegration repository.GithubAppOAuthIntegrationRepository
 	slackIntegration          repository.SlackIntegrationRepository
 	notificationConfig        repository.NotificationConfigRepository
-	event                     repository.EventRepository
+	buildEvent                repository.BuildEventRepository
+	kubeEvent                 repository.KubeEventRepository
 	projectUsage              repository.ProjectUsageRepository
 	onboarding                repository.ProjectOnboardingRepository
 	ceToken                   repository.CredentialsExchangeTokenRepository
@@ -133,8 +134,12 @@ func (t *GormRepository) NotificationConfig() repository.NotificationConfigRepos
 	return t.notificationConfig
 }
 
-func (t *GormRepository) Event() repository.EventRepository {
-	return t.event
+func (t *GormRepository) BuildEvent() repository.BuildEventRepository {
+	return t.buildEvent
+}
+
+func (t *GormRepository) KubeEvent() repository.KubeEventRepository {
+	return t.kubeEvent
 }
 
 func (t *GormRepository) ProjectUsage() repository.ProjectUsageRepository {
@@ -177,7 +182,8 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		githubAppOAuthIntegration: NewGithubAppOAuthIntegrationRepository(db),
 		slackIntegration:          NewSlackIntegrationRepository(db, key),
 		notificationConfig:        NewNotificationConfigRepository(db),
-		event:                     NewEventRepository(db),
+		buildEvent:                NewBuildEventRepository(db),
+		kubeEvent:                 NewKubeEventRepository(db, key),
 		projectUsage:              NewProjectUsageRepository(db),
 		onboarding:                NewProjectOnboardingRepository(db),
 		ceToken:                   NewCredentialsExchangeTokenRepository(db),

+ 2 - 1
internal/repository/repository.go

@@ -25,7 +25,8 @@ type Repository interface {
 	GithubAppOAuthIntegration() GithubAppOAuthIntegrationRepository
 	SlackIntegration() SlackIntegrationRepository
 	NotificationConfig() NotificationConfigRepository
-	Event() EventRepository
+	BuildEvent() BuildEventRepository
+	KubeEvent() KubeEventRepository
 	ProjectUsage() ProjectUsageRepository
 	Onboarding() ProjectOnboardingRepository
 	CredentialsExchangeToken() CredentialsExchangeTokenRepository

+ 37 - 9
internal/repository/test/event.go

@@ -1,36 +1,64 @@
 package test
 
 import (
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 )
 
-type EventRepository struct{}
+type BuildEventRepository struct{}
 
-func NewEventRepository(canQuery bool) repository.EventRepository {
-	return &EventRepository{}
+func NewBuildEventRepository(canQuery bool) repository.BuildEventRepository {
+	return &BuildEventRepository{}
 }
 
-func (n *EventRepository) CreateEventContainer(am *models.EventContainer) (*models.EventContainer, error) {
+func (n *BuildEventRepository) CreateEventContainer(am *models.EventContainer) (*models.EventContainer, error) {
 	panic("not implemented") // TODO: Implement
 }
 
-func (n *EventRepository) CreateSubEvent(am *models.SubEvent) (*models.SubEvent, error) {
+func (n *BuildEventRepository) CreateSubEvent(am *models.SubEvent) (*models.SubEvent, error) {
 	panic("not implemented") // TODO: Implement
 }
 
-func (n *EventRepository) ReadEventsByContainerID(id uint) ([]*models.SubEvent, error) {
+func (n *BuildEventRepository) ReadEventsByContainerID(id uint) ([]*models.SubEvent, error) {
 	panic("not implemented") // TODO: Implement
 }
 
-func (n *EventRepository) ReadEventContainer(id uint) (*models.EventContainer, error) {
+func (n *BuildEventRepository) ReadEventContainer(id uint) (*models.EventContainer, error) {
 	panic("not implemented") // TODO: Implement
 }
 
-func (n *EventRepository) ReadSubEvent(id uint) (*models.SubEvent, error) {
+func (n *BuildEventRepository) ReadSubEvent(id uint) (*models.SubEvent, error) {
 	panic("not implemented") // TODO: Implement
 }
 
-func (n *EventRepository) AppendEvent(container *models.EventContainer, event *models.SubEvent) error {
+func (n *BuildEventRepository) AppendEvent(container *models.EventContainer, event *models.SubEvent) error {
+	panic("not implemented") // TODO: Implement
+}
+
+type KubeEventRepository struct{}
+
+func NewKubeEventRepository(canQuery bool) repository.KubeEventRepository {
+	return &KubeEventRepository{}
+}
+
+func (n *KubeEventRepository) CreateEvent(event *models.KubeEvent) (*models.KubeEvent, error) {
+	panic("not implemented") // TODO: Implement
+}
+
+func (n *KubeEventRepository) ReadEvent(id uint, projID uint, clusterID uint) (*models.KubeEvent, error) {
+	panic("not implemented") // TODO: Implement
+}
+
+func (n *KubeEventRepository) ListEventsByProjectID(
+	projectID uint,
+	clusterID uint,
+	opts *types.ListKubeEventRequest,
+	shouldDecrypt bool,
+) ([]*models.KubeEvent, error) {
+	panic("not implemented") // TODO: Implement
+}
+
+func (n *KubeEventRepository) DeleteEvent(id uint) error {
 	panic("not implemented") // TODO: Implement
 }

+ 10 - 4
internal/repository/test/repository.go

@@ -29,7 +29,8 @@ type TestRepository struct {
 	githubAppOAuthIntegration repository.GithubAppOAuthIntegrationRepository
 	slackIntegration          repository.SlackIntegrationRepository
 	notificationConfig        repository.NotificationConfigRepository
-	event                     repository.EventRepository
+	buildEvent                repository.BuildEventRepository
+	kubeEvent                 repository.KubeEventRepository
 	projectUsage              repository.ProjectUsageRepository
 	onboarding                repository.ProjectOnboardingRepository
 	ceToken                   repository.CredentialsExchangeTokenRepository
@@ -131,8 +132,12 @@ func (t *TestRepository) NotificationConfig() repository.NotificationConfigRepos
 	return t.notificationConfig
 }
 
-func (t *TestRepository) Event() repository.EventRepository {
-	return t.event
+func (t *TestRepository) BuildEvent() repository.BuildEventRepository {
+	return t.buildEvent
+}
+
+func (t *TestRepository) KubeEvent() repository.KubeEventRepository {
+	return t.kubeEvent
 }
 
 func (t *TestRepository) ProjectUsage() repository.ProjectUsageRepository {
@@ -175,7 +180,8 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		githubAppOAuthIntegration: NewGithubAppOAuthIntegrationRepository(canQuery),
 		slackIntegration:          NewSlackIntegrationRepository(canQuery),
 		notificationConfig:        NewNotificationConfigRepository(canQuery),
-		event:                     NewEventRepository(canQuery),
+		buildEvent:                NewBuildEventRepository(canQuery),
+		kubeEvent:                 NewKubeEventRepository(canQuery),
 		projectUsage:              NewProjectUsageRepository(canQuery),
 		onboarding:                NewProjectOnboardingRepository(canQuery),
 		ceToken:                   NewCredentialsExchangeTokenRepository(canQuery),