From 9af8c4acd90b8813a5db4cfde0a26d45e2f25e17 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Wed, 20 Apr 2022 10:17:44 -0500 Subject: [PATCH 1/2] feat(middleware): add support for pipelines --- router/middleware/pipeline/context.go | 39 ++++ router/middleware/pipeline/context_test.go | 99 ++++++++ router/middleware/pipeline/doc.go | 12 + router/middleware/pipeline/pipeline.go | 73 ++++++ router/middleware/pipeline/pipeline_test.go | 240 ++++++++++++++++++++ 5 files changed, 463 insertions(+) create mode 100644 router/middleware/pipeline/context.go create mode 100644 router/middleware/pipeline/context_test.go create mode 100644 router/middleware/pipeline/doc.go create mode 100644 router/middleware/pipeline/pipeline.go create mode 100644 router/middleware/pipeline/pipeline_test.go diff --git a/router/middleware/pipeline/context.go b/router/middleware/pipeline/context.go new file mode 100644 index 000000000..6aab4037e --- /dev/null +++ b/router/middleware/pipeline/context.go @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/library" +) + +const key = "pipeline" + +// Setter defines a context that enables setting values. +type Setter interface { + Set(string, interface{}) +} + +// FromContext returns the Pipeline associated with this context. +func FromContext(c context.Context) *library.Pipeline { + value := c.Value(key) + if value == nil { + return nil + } + + b, ok := value.(*library.Pipeline) + if !ok { + return nil + } + + return b +} + +// ToContext adds the Pipeline to this context if it supports +// the Setter interface. +func ToContext(c Setter, b *library.Pipeline) { + c.Set(key, b) +} diff --git a/router/middleware/pipeline/context_test.go b/router/middleware/pipeline/context_test.go new file mode 100644 index 000000000..110be8ad3 --- /dev/null +++ b/router/middleware/pipeline/context_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "reflect" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types/library" +) + +func TestPipeline_FromContext(t *testing.T) { + // setup types + _pipeline := new(library.Pipeline) + + gin.SetMode(gin.TestMode) + _context, _ := gin.CreateTestContext(nil) + _context.Set(key, _pipeline) + + _emptyContext, _ := gin.CreateTestContext(nil) + + _nilContext, _ := gin.CreateTestContext(nil) + _nilContext.Set(key, nil) + + _typeContext, _ := gin.CreateTestContext(nil) + _typeContext.Set(key, 1) + + // setup tests + tests := []struct { + name string + context *gin.Context + want *library.Pipeline + }{ + { + name: "context", + context: _context, + want: _pipeline, + }, + { + name: "context with no value", + context: _emptyContext, + want: nil, + }, + { + name: "context with nil value", + context: _nilContext, + want: nil, + }, + { + name: "context with wrong value type", + context: _typeContext, + want: nil, + }, + } + + // run tests + for _, test := range tests { + got := FromContext(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromContext for %s is %v, want %v", test.name, got, test.want) + } + } +} + +func TestPipeline_ToContext(t *testing.T) { + // setup types + _pipeline := new(library.Pipeline) + + gin.SetMode(gin.TestMode) + _context, _ := gin.CreateTestContext(nil) + + // setup tests + tests := []struct { + name string + context *gin.Context + want *library.Pipeline + }{ + { + name: "context", + context: _context, + want: _pipeline, + }, + } + + // run tests + for _, test := range tests { + ToContext(test.context, test.want) + + got := test.context.Value(key) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ToContext for %s is %v, want %v", test.name, got, test.want) + } + } +} diff --git a/router/middleware/pipeline/doc.go b/router/middleware/pipeline/doc.go new file mode 100644 index 000000000..70c59a3a4 --- /dev/null +++ b/router/middleware/pipeline/doc.go @@ -0,0 +1,12 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package pipeline provides the ability for inserting +// Vela pipeline resources into or extracting Vela pipeline +// resources from the middleware chain for the API. +// +// Usage: +// +// import "github.com/go-vela/server/router/middleware/pipeline" +package pipeline diff --git a/router/middleware/pipeline/pipeline.go b/router/middleware/pipeline/pipeline.go new file mode 100644 index 000000000..b4cdca1c4 --- /dev/null +++ b/router/middleware/pipeline/pipeline.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// Retrieve gets the pipeline in the given context. +func Retrieve(c *gin.Context) *library.Pipeline { + return FromContext(c) +} + +// Establish sets the pipeline in the given context. +func Establish() gin.HandlerFunc { + return func(c *gin.Context) { + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + if r == nil { + retErr := fmt.Errorf("repo %s/%s not found", c.Param("org"), c.Param("repo")) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + p := c.Param("pipeline") + if len(p) == 0 { + retErr := fmt.Errorf("no pipeline parameter provided") + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p, + "repo": r.GetName(), + "user": u.GetName(), + }).Debugf("reading pipeline %s/%s", r.GetFullName(), p) + + pipeline, err := database.FromContext(c).GetPipelineForRepo(p, r) + if err != nil { + retErr := fmt.Errorf("unable to read pipeline %s/%s: %w", r.GetFullName(), p, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + ToContext(c, pipeline) + + c.Next() + } +} diff --git a/router/middleware/pipeline/pipeline_test.go b/router/middleware/pipeline/pipeline_test.go new file mode 100644 index 000000000..1658520c2 --- /dev/null +++ b/router/middleware/pipeline/pipeline_test.go @@ -0,0 +1,240 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/types/library" +) + +func TestPipeline_Retrieve(t *testing.T) { + // setup types + _pipeline := new(library.Pipeline) + + gin.SetMode(gin.TestMode) + _context, _ := gin.CreateTestContext(nil) + + // setup tests + tests := []struct { + name string + context *gin.Context + want *library.Pipeline + }{ + { + name: "context", + context: _context, + want: _pipeline, + }, + } + + // run tests + for _, test := range tests { + ToContext(test.context, test.want) + + got := Retrieve(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Retrieve for %s is %v, want %v", test.name, got, test.want) + } + } +} + +func TestPipeline_Establish(t *testing.T) { + // setup types + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + want := new(library.Pipeline) + want.SetID(1) + want.SetRepoID(1) + want.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + want.SetFlavor("") + want.SetPlatform("") + want.SetRef("refs/heads/master") + want.SetType("yaml") + want.SetVersion("1") + want.SetExternalSecrets(false) + want.SetInternalSecrets(false) + want.SetServices(false) + want.SetStages(false) + want.SetSteps(false) + want.SetTemplates(false) + want.SetData([]byte{}) + + got := new(library.Pipeline) + + // setup database + db, _ := sqlite.NewTest() + + defer func() { + db.Sqlite.Exec("delete from repos;") + db.Sqlite.Exec("delete from pipelines;") + _sql, _ := db.Sqlite.DB() + _sql.Close() + }() + + _ = db.CreateRepo(r) + _ = db.CreatePipeline(want) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar/48afb5bdc41ad69bf22588491333f7cf71135163", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(Establish()) + engine.GET("/pipelines/:org/:repo/:pipeline", func(c *gin.Context) { + got = Retrieve(c) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Establish is %v, want %v", got, want) + } +} + +func TestPipeline_Establish_NoRepo(t *testing.T) { + // setup database + db, _ := sqlite.NewTest() + defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar/48afb5bdc41ad69bf22588491333f7cf71135163", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(Establish()) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusNotFound { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusNotFound) + } +} + +func TestPipeline_Establish_NoPipelineParameter(t *testing.T) { + // setup types + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + // setup database + db, _ := sqlite.NewTest() + + defer func() { + db.Sqlite.Exec("delete from repos;") + _sql, _ := db.Sqlite.DB() + _sql.Close() + }() + + _ = db.CreateRepo(r) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(Establish()) + engine.GET("/pipelines/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusBadRequest { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusBadRequest) + } +} + +func TestPipeline_Establish_NoPipeline(t *testing.T) { + // setup types + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + // setup database + db, _ := sqlite.NewTest() + + defer func() { + db.Sqlite.Exec("delete from repos;") + _sql, _ := db.Sqlite.DB() + _sql.Close() + }() + + _ = db.CreateRepo(r) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar/148afb5bdc41ad69bf22588491333f7cf71135163", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(Establish()) + engine.GET("/pipelines/:org/:repo/:pipeline", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusNotFound { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusNotFound) + } +} From 6d71246aa486f27cba806107180c90963f89f119 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Wed, 20 Apr 2022 10:41:34 -0500 Subject: [PATCH 2/2] test: use subtests for table tests --- router/middleware/pipeline/context_test.go | 22 ++++++++++++--------- router/middleware/pipeline/pipeline_test.go | 12 ++++++----- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/router/middleware/pipeline/context_test.go b/router/middleware/pipeline/context_test.go index 110be8ad3..ddc5b8bbd 100644 --- a/router/middleware/pipeline/context_test.go +++ b/router/middleware/pipeline/context_test.go @@ -58,11 +58,13 @@ func TestPipeline_FromContext(t *testing.T) { // run tests for _, test := range tests { - got := FromContext(test.context) + t.Run(test.name, func(t *testing.T) { + got := FromContext(test.context) - if !reflect.DeepEqual(got, test.want) { - t.Errorf("FromContext for %s is %v, want %v", test.name, got, test.want) - } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromContext for %s is %v, want %v", test.name, got, test.want) + } + }) } } @@ -88,12 +90,14 @@ func TestPipeline_ToContext(t *testing.T) { // run tests for _, test := range tests { - ToContext(test.context, test.want) + t.Run(test.name, func(t *testing.T) { + ToContext(test.context, test.want) - got := test.context.Value(key) + got := test.context.Value(key) - if !reflect.DeepEqual(got, test.want) { - t.Errorf("ToContext for %s is %v, want %v", test.name, got, test.want) - } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ToContext for %s is %v, want %v", test.name, got, test.want) + } + }) } } diff --git a/router/middleware/pipeline/pipeline_test.go b/router/middleware/pipeline/pipeline_test.go index 1658520c2..1025cdb30 100644 --- a/router/middleware/pipeline/pipeline_test.go +++ b/router/middleware/pipeline/pipeline_test.go @@ -40,13 +40,15 @@ func TestPipeline_Retrieve(t *testing.T) { // run tests for _, test := range tests { - ToContext(test.context, test.want) + t.Run(test.name, func(t *testing.T) { + ToContext(test.context, test.want) - got := Retrieve(test.context) + got := Retrieve(test.context) - if !reflect.DeepEqual(got, test.want) { - t.Errorf("Retrieve for %s is %v, want %v", test.name, got, test.want) - } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Retrieve for %s is %v, want %v", test.name, got, test.want) + } + }) } }