Feature/dron 101 cards 2 (#3158)

* refactor create / find / delete end points for cards
This commit is contained in:
Eoin McAfee 2021-10-26 08:10:01 +01:00 committed by GitHub
parent a2210384f9
commit df2da1c646
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 362 additions and 640 deletions

View file

@ -17,48 +17,19 @@ package core
import (
"context"
"io"
"github.com/drone/drone/handler/api/errors"
)
var (
errCardStepInvalid = errors.New("No Step ID Provided")
errCardBuildInvalid = errors.New("No Build ID Provided")
errCardSchemaInvalid = errors.New("No Card Schema Has Been Provided")
)
type Card struct {
Id int64 `json:"id,omitempty"`
Build int64 `json:"build,omitempty"`
Stage int64 `json:"stage,omitempty"`
Step int64 `json:"step,omitempty"`
Schema string `json:"schema,omitempty"`
}
type CardData struct {
Id int64 `json:"id,omitempty"`
Data []byte `json:"card_data"`
}
// CardStore manages repository cards.
type CardStore interface {
FindByBuild(ctx context.Context, build int64) ([]*Card, error)
Find(ctx context.Context, step int64) (*Card, error)
FindData(ctx context.Context, id int64) (io.ReadCloser, error)
Create(ctx context.Context, card *Card, data io.ReadCloser) error
Delete(ctx context.Context, id int64) error
}
// Find returns a card data stream from the datastore.
Find(ctx context.Context, step int64) (io.ReadCloser, error)
// Validate validates the required fields and formats.
func (c *Card) Validate() error {
switch {
case c.Step == 0:
return errCardStepInvalid
case c.Build == 0:
return errCardBuildInvalid
case len(c.Schema) == 0:
return errCardSchemaInvalid
default:
return nil
}
// Create copies the card stream from Reader r to the datastore.
Create(ctx context.Context, step int64, r io.Reader) error
// Update copies the card stream from Reader r to the datastore.
Update(ctx context.Context, step int64, r io.Reader) error
// Delete purges the card data from the datastore.
Delete(ctx context.Context, step int64) error
}

View file

@ -33,6 +33,7 @@ type (
DependsOn []string `json:"depends_on,omitempty"`
Image string `json:"image,omitempty"`
Detached bool `json:"detached,omitempty"`
Schema string `json:"schema,omitempty"`
}
// StepStore persists build step information to storage.

View file

@ -291,9 +291,7 @@ func (s Server) Handler() http.Handler {
})
r.Route("/cards", func(r chi.Router) {
r.Get("/{build}", card.HandleFindAll(s.Builds, s.Card, s.Repos))
r.Get("/{build}/{stage}/{step}", card.HandleFind(s.Builds, s.Card, s.Stages, s.Steps, s.Repos))
r.Get("/{build}/{stage}/{step}/json", card.HandleFindData(s.Builds, s.Card, s.Stages, s.Steps, s.Repos))
r.With(
acl.CheckAdminAccess(),
).Post("/{build}/{stage}/{step}", card.HandleCreate(s.Builds, s.Card, s.Stages, s.Steps, s.Repos))

View file

@ -85,29 +85,24 @@ func HandleCreate(
return
}
c := &core.Card{
Build: build.ID,
Stage: stage.ID,
Step: step.ID,
Schema: in.Schema,
}
err = c.Validate()
if err != nil {
render.BadRequest(w, err)
return
}
data := ioutil.NopCloser(
bytes.NewBuffer([]byte(in.Data)),
bytes.NewBuffer(in.Data),
)
err = cardStore.Create(r.Context(), c, data)
/// create card
err = cardStore.Create(r.Context(), step.ID, data)
if err != nil {
render.InternalError(w, err)
return
}
render.JSON(w, c, 200)
// add schema
step.Schema = in.Schema
err = stepStore.Update(r.Context(), step)
if err != nil {
render.InternalError(w, err)
return
}
render.JSON(w, step.ID, 200)
}
}

View file

@ -10,7 +10,6 @@ import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
@ -24,6 +23,11 @@ import (
"github.com/google/go-cmp/cmp"
)
type card struct {
Id int64 `json:"id,omitempty"`
Data []byte `json:"card_data"`
}
var (
dummyRepo = &core.Repository{
ID: 1,
@ -42,23 +46,12 @@ var (
dummyStep = &core.Step{
ID: 1,
StageID: 1,
Schema: "https://myschema.com",
}
dummyCreateCard = &core.Card{
Schema: "https://myschema.com",
dummyCard = &card{
Id: dummyStep.ID,
Data: []byte("{\"type\": \"AdaptiveCard\"}"),
}
dummyCard = &core.Card{
Id: 1,
Build: 1,
Stage: 1,
Step: 1,
Schema: "https://myschema.com",
}
dummyCardList = []*core.Card{
dummyCard,
}
dummyCardData = ioutil.NopCloser(
bytes.NewBuffer([]byte("{\"type\": \"AdaptiveCard\"}")),
)
)
func TestHandleCreate(t *testing.T) {
@ -76,6 +69,7 @@ func TestHandleCreate(t *testing.T) {
step := mock.NewMockStepStore(controller)
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
step.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
@ -88,7 +82,7 @@ func TestHandleCreate(t *testing.T) {
c.URLParams.Add("step", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
json.NewEncoder(in).Encode(dummyCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/", in)
@ -140,12 +134,12 @@ func TestHandleCreate_CreateError(t *testing.T) {
stage := mock.NewMockStageStore(controller)
stage.EXPECT().FindNumber(gomock.Any(), dummyBuild.ID, gomock.Any()).Return(dummyStage, nil)
step := mock.NewMockStepStore(controller)
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.ErrNotFound)
step := mock.NewMockStepStore(controller)
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
c := new(chi.Context)
c.URLParams.Add("owner", "octocat")
c.URLParams.Add("name", "hello-world")
@ -153,7 +147,7 @@ func TestHandleCreate_CreateError(t *testing.T) {
c.URLParams.Add("stage", "1")
c.URLParams.Add("step", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
json.NewEncoder(in).Encode(dummyCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in)

View file

@ -71,12 +71,12 @@ func HandleDelete(
return
}
card, err := cardStore.Find(r.Context(), step.ID)
_, err = cardStore.Find(r.Context(), step.ID)
if err != nil {
render.NotFound(w, err)
return
}
err = cardStore.Delete(r.Context(), card.Id)
err = cardStore.Delete(r.Context(), step.ID)
if err != nil {
render.InternalError(w, err)
return

View file

@ -7,8 +7,10 @@
package card
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
@ -38,7 +40,9 @@ func TestHandleDelete(t *testing.T) {
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(dummyCard, nil)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(ioutil.NopCloser(
bytes.NewBuffer(dummyCard.Data),
), nil)
card.EXPECT().Delete(gomock.Any(), dummyCard.Id).Return(nil)
c := new(chi.Context)
@ -121,7 +125,9 @@ func TestHandleDelete_DeleteError(t *testing.T) {
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(dummyCard, nil)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(ioutil.NopCloser(
bytes.NewBuffer(dummyCard.Data),
), nil)
card.EXPECT().Delete(gomock.Any(), dummyCard.Id).Return(errors.ErrNotFound)
c := new(chi.Context)

View file

@ -7,6 +7,7 @@
package card
import (
"io"
"net/http"
"strconv"
@ -69,11 +70,13 @@ func HandleFind(
return
}
card, err := cardStore.Find(r.Context(), step.ID)
cardData, err := cardStore.Find(r.Context(), step.ID)
if err != nil {
render.NotFound(w, err)
return
}
render.JSON(w, card, 200)
w.Header().Set("Content-Type", "application/json")
io.Copy(w, cardData)
cardData.Close()
}
}

View file

@ -1,55 +0,0 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
// +build !oss
package card
import (
"net/http"
"strconv"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/go-chi/chi"
)
// HandleFindAll returns an http.HandlerFunc that writes a json-encoded
func HandleFindAll(
buildStore core.BuildStore,
cardStore core.CardStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
namespace = chi.URLParam(r, "owner")
name = chi.URLParam(r, "name")
)
buildNumber, err := strconv.ParseInt(chi.URLParam(r, "build"), 10, 64)
if err != nil {
render.BadRequest(w, err)
return
}
repo, err := repoStore.FindName(r.Context(), namespace, name)
if err != nil {
render.NotFound(w, err)
return
}
build, err := buildStore.FindNumber(r.Context(), repo.ID, buildNumber)
if err != nil {
render.NotFound(w, err)
return
}
list, err := cardStore.FindByBuild(r.Context(), build.ID)
if err != nil {
render.NotFound(w, err)
return
}
render.JSON(w, list, 200)
}
}

View file

@ -1,97 +0,0 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
// +build !oss
package card
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/drone/drone/handler/api/errors"
"github.com/drone/drone/mock"
"github.com/go-chi/chi"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
)
func TestHandleFindAll(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
repos := mock.NewMockRepositoryStore(controller)
repos.EXPECT().FindName(gomock.Any(), "octocat", "hello-world").Return(dummyRepo, nil)
build := mock.NewMockBuildStore(controller)
build.EXPECT().FindNumber(gomock.Any(), dummyBuild.ID, gomock.Any()).Return(dummyBuild, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().FindByBuild(gomock.Any(), dummyBuild.ID).Return(dummyCardList, nil)
c := new(chi.Context)
c.URLParams.Add("owner", "octocat")
c.URLParams.Add("name", "hello-world")
c.URLParams.Add("build", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleFindAll(build, card, repos).ServeHTTP(w, r)
if got, want := w.Code, http.StatusOK; want != got {
t.Errorf("Want response code %d, got %d", want, got)
}
}
func TestHandleFindAll_CardsNotFound(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
repos := mock.NewMockRepositoryStore(controller)
repos.EXPECT().FindName(gomock.Any(), "octocat", "hello-world").Return(dummyRepo, nil)
build := mock.NewMockBuildStore(controller)
build.EXPECT().FindNumber(gomock.Any(), dummyBuild.ID, gomock.Any()).Return(dummyBuild, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().FindByBuild(gomock.Any(), dummyBuild.ID).Return(nil, errors.ErrNotFound)
c := new(chi.Context)
c.URLParams.Add("owner", "octocat")
c.URLParams.Add("name", "hello-world")
c.URLParams.Add("build", "1")
c.URLParams.Add("stage", "1")
c.URLParams.Add("step", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleFindAll(build, card, repos).ServeHTTP(w, r)
if got, want := w.Code, http.StatusNotFound; want != got {
t.Errorf("Want response code %d, got %d", want, got)
}
got, want := new(errors.Error), errors.ErrNotFound
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}

View file

@ -1,87 +0,0 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
// +build !oss
package card
import (
"io"
"net/http"
"strconv"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/go-chi/chi"
)
// HandleFindData returns an http.HandlerFunc that writes a json-encoded
func HandleFindData(
buildStore core.BuildStore,
cardStore core.CardStore,
stageStore core.StageStore,
stepStore core.StepStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
namespace = chi.URLParam(r, "owner")
name = chi.URLParam(r, "name")
)
buildNumber, err := strconv.ParseInt(chi.URLParam(r, "build"), 10, 64)
if err != nil {
render.BadRequest(w, err)
return
}
stageNumber, err := strconv.Atoi(chi.URLParam(r, "stage"))
if err != nil {
render.BadRequest(w, err)
return
}
stepNumber, err := strconv.Atoi(chi.URLParam(r, "step"))
if err != nil {
render.BadRequest(w, err)
return
}
repo, err := repoStore.FindName(r.Context(), namespace, name)
if err != nil {
render.NotFound(w, err)
return
}
build, err := buildStore.FindNumber(r.Context(), repo.ID, buildNumber)
if err != nil {
render.NotFound(w, err)
return
}
stage, err := stageStore.FindNumber(r.Context(), build.ID, stageNumber)
if err != nil {
render.NotFound(w, err)
return
}
step, err := stepStore.FindNumber(r.Context(), stage.ID, stepNumber)
if err != nil {
render.NotFound(w, err)
return
}
card, err := cardStore.Find(r.Context(), step.ID)
if err != nil {
render.NotFound(w, err)
return
}
cardData, err := cardStore.FindData(r.Context(), card.Id)
if err != nil {
render.NotFound(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
io.Copy(w, cardData)
cardData.Close()
}
}

View file

@ -1,63 +0,0 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
// +build !oss
package card
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/drone/drone/mock"
"github.com/go-chi/chi"
"github.com/golang/mock/gomock"
)
func TestHandleFindCardData(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
repos := mock.NewMockRepositoryStore(controller)
repos.EXPECT().FindName(gomock.Any(), "octocat", "hello-world").Return(dummyRepo, nil)
build := mock.NewMockBuildStore(controller)
build.EXPECT().FindNumber(gomock.Any(), dummyBuild.ID, gomock.Any()).Return(dummyBuild, nil)
stage := mock.NewMockStageStore(controller)
stage.EXPECT().FindNumber(gomock.Any(), dummyBuild.ID, gomock.Any()).Return(dummyStage, nil)
step := mock.NewMockStepStore(controller)
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(dummyCard, nil)
card.EXPECT().FindData(gomock.Any(), dummyCard.Id).Return(dummyCardData, nil)
c := new(chi.Context)
c.URLParams.Add("owner", "octocat")
c.URLParams.Add("name", "hello-world")
c.URLParams.Add("build", "1")
c.URLParams.Add("stage", "1")
c.URLParams.Add("step", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleFindData(build, card, stage, step, repos).ServeHTTP(w, r)
if got, want := w.Code, http.StatusOK; want != got {
t.Errorf("Want response code %d, got %d", want, got)
}
}

View file

@ -10,6 +10,7 @@ import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
@ -39,7 +40,9 @@ func TestHandleFind(t *testing.T) {
step.EXPECT().FindNumber(gomock.Any(), dummyStage.ID, gomock.Any()).Return(dummyStep, nil)
card := mock.NewMockCardStore(controller)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(dummyCard, nil)
card.EXPECT().Find(gomock.Any(), dummyStep.ID).Return(ioutil.NopCloser(
bytes.NewBuffer(dummyCard.Data),
), nil)
c := new(chi.Context)
c.URLParams.Add("owner", "octocat")
@ -49,7 +52,7 @@ func TestHandleFind(t *testing.T) {
c.URLParams.Add("step", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
json.NewEncoder(in).Encode(dummyCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in)
@ -90,7 +93,7 @@ func TestHandleFind_CardNotFound(t *testing.T) {
c.URLParams.Add("step", "1")
in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyCreateCard)
json.NewEncoder(in).Encode(dummyCard)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in)

View file

@ -2919,7 +2919,7 @@ func (m *MockCardStore) EXPECT() *MockCardStoreMockRecorder {
}
// Create mocks base method.
func (m *MockCardStore) Create(arg0 context.Context, arg1 *core.Card, arg2 io.ReadCloser) error {
func (m *MockCardStore) Create(arg0 context.Context, arg1 int64, arg2 io.Reader) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
@ -2947,10 +2947,10 @@ func (mr *MockCardStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call
}
// Find mocks base method.
func (m *MockCardStore) Find(arg0 context.Context, arg1 int64) (*core.Card, error) {
func (m *MockCardStore) Find(arg0 context.Context, arg1 int64) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", arg0, arg1)
ret0, _ := ret[0].(*core.Card)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -2961,32 +2961,16 @@ func (mr *MockCardStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockCardStore)(nil).Find), arg0, arg1)
}
// FindByBuild mocks base method.
func (m *MockCardStore) FindByBuild(arg0 context.Context, arg1 int64) ([]*core.Card, error) {
// Update mocks base method.
func (m *MockCardStore) Update(arg0 context.Context, arg1 int64, arg2 io.Reader) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByBuild", arg0, arg1)
ret0, _ := ret[0].([]*core.Card)
ret1, _ := ret[1].(error)
return ret0, ret1
ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// FindByBuild indicates an expected call of FindByBuild.
func (mr *MockCardStoreMockRecorder) FindByBuild(arg0, arg1 interface{}) *gomock.Call {
// Update indicates an expected call of Update.
func (mr *MockCardStoreMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByBuild", reflect.TypeOf((*MockCardStore)(nil).FindByBuild), arg0, arg1)
}
// FindData mocks base method.
func (m *MockCardStore) FindData(arg0 context.Context, arg1 int64) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindData", arg0, arg1)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindData indicates an expected call of FindData.
func (mr *MockCardStoreMockRecorder) FindData(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindData", reflect.TypeOf((*MockCardStore)(nil).FindData), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCardStore)(nil).Update), arg0, arg1, arg2)
}

View file

@ -26,113 +26,78 @@ type cardStore struct {
db *db.DB
}
func (c *cardStore) FindByBuild(ctx context.Context, build int64) ([]*core.Card, error) {
var out []*core.Card
err := c.db.View(func(queryer db.Queryer, binder db.Binder) error {
params := map[string]interface{}{
"card_build": build,
}
stmt, args, err := binder.BindNamed(queryByBuild, params)
if err != nil {
return err
}
rows, err := queryer.Query(stmt, args...)
if err != nil {
return err
}
out, err = scanRows(rows)
return err
})
return out, err
type card struct {
Id int64 `json:"id,omitempty"`
Data []byte `json:"card_data"`
}
func (c cardStore) Find(ctx context.Context, step int64) (*core.Card, error) {
out := &core.Card{Step: step}
func (c cardStore) Find(ctx context.Context, step int64) (io.ReadCloser, error) {
out := &card{Id: step}
err := c.db.View(func(queryer db.Queryer, binder db.Binder) error {
params, err := toParams(out)
if err != nil {
return err
}
query, args, err := binder.BindNamed(queryByStep, params)
if err != nil {
return err
}
row := queryer.QueryRow(query, args...)
return scanRow(row, out)
})
return out, err
}
func (c cardStore) FindData(ctx context.Context, id int64) (io.ReadCloser, error) {
out := &core.CardData{}
err := c.db.View(func(queryer db.Queryer, binder db.Binder) error {
params := map[string]interface{}{
"card_id": id,
}
query, args, err := binder.BindNamed(queryKey, params)
if err != nil {
return err
}
row := queryer.QueryRow(query, args...)
return scanRowCardDataOnly(row, out)
return scanRow(row, out)
})
return ioutil.NopCloser(
bytes.NewBuffer(out.Data),
), err
}
func (c cardStore) Create(ctx context.Context, card *core.Card, data io.ReadCloser) error {
if c.db.Driver() == db.Postgres {
return c.createPostgres(ctx, card, data)
func (c cardStore) Create(ctx context.Context, step int64, r io.Reader) error {
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
return c.create(ctx, card, data)
}
func (c *cardStore) create(ctx context.Context, card *core.Card, data io.ReadCloser) error {
return c.db.Lock(func(execer db.Execer, binder db.Binder) error {
cardData, err := ioutil.ReadAll(data)
if err != nil {
return err
}
params, err := toSaveCardParams(card, cardData)
if err != nil {
return err
in := &card{
Id: step,
Data: data,
}
params, err := toParams(in)
stmt, args, err := binder.BindNamed(stmtInsert, params)
if err != nil {
return err
}
res, err := execer.Exec(stmt, args...)
if err != nil {
return err
}
card.Id, err = res.LastInsertId()
_, err = execer.Exec(stmt, args...)
return err
})
}
func (c *cardStore) createPostgres(ctx context.Context, card *core.Card, data io.ReadCloser) error {
func (c *cardStore) Update(ctx context.Context, step int64, r io.Reader) error {
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
return c.db.Lock(func(execer db.Execer, binder db.Binder) error {
cardData, err := ioutil.ReadAll(data)
card := &card{
Id: step,
Data: data,
}
params, err := toParams(card)
if err != nil {
return err
}
params, err := toSaveCardParams(card, cardData)
stmt, args, err := binder.BindNamed(stmtUpdate, params)
if err != nil {
return err
}
stmt, args, err := binder.BindNamed(stmtInsertPostgres, params)
if err != nil {
return err
}
return execer.QueryRow(stmt, args...).Scan(&card.Id)
_, err = execer.Exec(stmt, args...)
return err
})
}
func (c cardStore) Delete(ctx context.Context, id int64) error {
func (c cardStore) Delete(ctx context.Context, step int64) error {
return c.db.Lock(func(execer db.Execer, binder db.Binder) error {
params := map[string]interface{}{
"card_id": id,
params := &card{
Id: step,
}
stmt, args, err := binder.BindNamed(stmtDelete, params)
if err != nil {
@ -144,32 +109,12 @@ func (c cardStore) Delete(ctx context.Context, id int64) error {
}
const queryBase = `
SELECT
card_id
,card_build
,card_stage
,card_step
,card_schema
`
const queryCardData = `
SELECT
card_id
,card_data
`
const queryByBuild = queryBase + `
FROM cards
WHERE card_build = :card_build
`
const queryByStep = queryBase + `
FROM cards
WHERE card_step = :card_step
LIMIT 1
`
const queryKey = queryCardData + `
const queryKey = queryBase + `
FROM cards
WHERE card_id = :card_id
LIMIT 1
@ -177,20 +122,20 @@ LIMIT 1
const stmtInsert = `
INSERT INTO cards (
card_build
,card_stage
,card_step
,card_schema
card_id
,card_data
) VALUES (
:card_build
,:card_stage
,:card_step
,:card_schema
:card_id
,:card_data
)
`
const stmtUpdate = `
UPDATE cards
SET card_data = :card_data
WHERE card_id = :card_id
`
const stmtDelete = `
DELETE FROM cards
WHERE card_id = :card_id

View file

@ -30,22 +30,18 @@ func New(db *db.DB) core.CardStore {
type noop struct{}
func (noop) FindByBuild(ctx context.Context, build int64) ([]*core.Card, error) {
func (noop) Find(ctx context.Context, step int64) (io.ReadCloser, error) {
return nil, nil
}
func (noop) Find(ctx context.Context, step int64) (*core.Card, error) {
return nil, nil
}
func (noop) FindData(ctx context.Context, id int64) (io.ReadCloser, error) {
return nil, nil
}
func (noop) Create(ctx context.Context, card *core.Card, data io.ReadCloser) error {
func (noop) Create(ctx context.Context, step int64, r io.Reader) error {
return nil
}
func (noop) Delete(ctx context.Context, id int64) error {
func (noop) Update(ctx context.Context, step int64, r io.Reader) error {
return nil
}
func (noop) Delete(ctx context.Context, step int64) error {
return nil
}

View file

@ -8,7 +8,10 @@ import (
"testing"
"github.com/drone/drone/core"
"github.com/drone/drone/store/build"
"github.com/drone/drone/store/repos"
"github.com/drone/drone/store/shared/db/dbtest"
"github.com/drone/drone/store/step"
)
var noContext = context.TODO()
@ -24,79 +27,46 @@ func TestCard(t *testing.T) {
dbtest.Disconnect(conn)
}()
// seed with a dummy repository
dummyRepo := &core.Repository{UID: "1", Slug: "octocat/hello-world"}
repos := repos.New(conn)
repos.Create(noContext, dummyRepo)
// seed with a dummy stage
stage := &core.Stage{Number: 1}
stages := []*core.Stage{stage}
// seed with a dummy build
dummyBuild := &core.Build{Number: 1, RepoID: dummyRepo.ID}
builds := build.New(conn)
builds.Create(noContext, dummyBuild, stages)
// seed with a dummy step
dummyStep := &core.Step{Number: 1, StageID: stage.ID}
steps := step.New(conn)
steps.Create(noContext, dummyStep)
store := New(conn).(*cardStore)
t.Run("Create", testCardCreate(store))
t.Run("Create", testCardCreate(store, dummyStep))
t.Run("Find", testFindCard(store, dummyStep))
t.Run("Update", testLogsUpdate(store, dummyStep))
}
func testCard(item *core.Card) func(t *testing.T) {
func testCardCreate(store *cardStore, step *core.Step) func(t *testing.T) {
return func(t *testing.T) {
if got, want := item.Schema, "https://myschema.com"; got != want {
t.Errorf("Want card schema %q, got %q", want, got)
}
if got, want := item.Build, int64(1); got != want {
t.Errorf("Want card build number %q, got %q", want, got)
}
if got, want := item.Stage, int64(2); got != want {
t.Errorf("Want card stage number %q, got %q", want, got)
}
if got, want := item.Step, int64(3); got != want {
t.Errorf("Want card step number %q, got %q", want, got)
}
}
}
func testCardCreate(store *cardStore) func(t *testing.T) {
return func(t *testing.T) {
item := &core.Card{
Id: 1,
Build: 1,
Stage: 2,
Step: 3,
Schema: "https://myschema.com",
}
buf := ioutil.NopCloser(
bytes.NewBuffer([]byte("{\"type\": \"AdaptiveCard\"}")),
)
err := store.Create(noContext, item, buf)
err := store.Create(noContext, step.ID, buf)
if err != nil {
t.Error(err)
}
if item.Id == 0 {
t.Errorf("Want card Id assigned, got %d", item.Id)
}
t.Run("FindByBuild", testFindCardByBuild(store))
t.Run("Find", testFindCard(store))
t.Run("FindData", testFindCardData(store))
t.Run("Delete", testCardDelete(store))
}
}
func testFindCardByBuild(card *cardStore) func(t *testing.T) {
return func(t *testing.T) {
item, err := card.FindByBuild(noContext, 1)
if err != nil {
t.Error(err)
} else {
t.Run("Fields", testCard(item[0]))
}
}
}
func testFindCard(card *cardStore) func(t *testing.T) {
func testFindCard(card *cardStore, step *core.Step) func(t *testing.T) {
return func(t *testing.T) {
item, err := card.Find(noContext, 3)
if err != nil {
t.Error(err)
} else {
t.Run("Fields", testCard(item))
}
}
}
func testFindCardData(card *cardStore) func(t *testing.T) {
return func(t *testing.T) {
r, err := card.FindData(noContext, 1)
r, err := card.Find(noContext, step.ID)
if err != nil {
t.Error(err)
} else {
@ -112,19 +82,38 @@ func testFindCardData(card *cardStore) func(t *testing.T) {
}
}
func testCardDelete(store *cardStore) func(t *testing.T) {
func testLogsUpdate(store *cardStore, step *core.Step) func(t *testing.T) {
return func(t *testing.T) {
card, err := store.Find(noContext, 3)
buf := bytes.NewBufferString("hola mundo")
err := store.Update(noContext, step.ID, buf)
if err != nil {
t.Error(err)
return
}
err = store.Delete(noContext, card.Id)
r, err := store.Find(noContext, step.ID)
if err != nil {
t.Error(err)
return
}
_, err = store.Find(noContext, card.Step)
data, err := ioutil.ReadAll(r)
if err != nil {
t.Error(err)
return
}
if got, want := string(data), "hola mundo"; got != want {
t.Errorf("Want updated log output stream %q, got %q", want, got)
}
}
}
func testLogsDelete(store *cardStore, step *core.Step) func(t *testing.T) {
return func(t *testing.T) {
err := store.Delete(noContext, step.ID)
if err != nil {
t.Error(err)
return
}
_, err = store.Find(noContext, step.ID)
if got, want := sql.ErrNoRows, err; got != want {
t.Errorf("Want sql.ErrNoRows, got %v", got)
return

View file

@ -7,73 +7,27 @@
package card
import (
"database/sql"
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db"
)
// helper function converts the card structure to a set
// of named query parameters.
func toParams(card *core.Card) (map[string]interface{}, error) {
func toParams(card *card) (map[string]interface{}, error) {
return map[string]interface{}{
"card_id": card.Id,
"card_build": card.Build,
"card_stage": card.Stage,
"card_step": card.Step,
"card_schema": card.Schema,
}, nil
}
// helper function converts the card structure to a set
// of named query parameters.
func toSaveCardParams(card *core.Card, data []byte) (map[string]interface{}, error) {
return map[string]interface{}{
"card_id": card.Id,
"card_build": card.Build,
"card_stage": card.Stage,
"card_step": card.Step,
"card_schema": card.Schema,
"card_data": data,
"card_id": card.Id,
"card_data": card.Data,
}, nil
}
// helper function scans the sql.Row and copies the column
// values to the destination object.
func scanRow(scanner db.Scanner, dst *core.Card) error {
func scanRow(scanner db.Scanner, dst *card) error {
err := scanner.Scan(
&dst.Id,
&dst.Build,
&dst.Stage,
&dst.Step,
&dst.Schema,
&dst.Data,
)
if err != nil {
return err
}
return nil
}
func scanRowCardDataOnly(scanner db.Scanner, dst *core.CardData) error {
return scanner.Scan(
&dst.Id,
&dst.Data,
)
}
// helper function scans the sql.Row and copies the column
// values to the destination object.
func scanRows(rows *sql.Rows) ([]*core.Card, error) {
defer rows.Close()
card := []*core.Card{}
for rows.Next() {
tem := new(core.Card)
err := scanRow(rows, tem)
if err != nil {
return nil, err
}
card = append(card, tem)
}
return card, nil
}

View file

@ -185,8 +185,24 @@ var migrations = []struct {
stmt: createTableCards,
},
{
name: "create-index-cards-card_build",
stmt: createIndexCardsCardbuild,
name: "create-index-cards-card-build",
stmt: createIndexCardsCardBuild,
},
{
name: "create-index-cards-card_step",
stmt: createIndexCardsCardstep,
},
{
name: "drop-table-cards",
stmt: dropTableCards,
},
{
name: "alter-table-steps-add-column-step_schema",
stmt: alterTableStepsAddColumnStepschema,
},
{
name: "create-new-table-cards",
stmt: createNewTableCards,
},
}
@ -746,6 +762,32 @@ CREATE TABLE IF NOT EXISTS cards (
);
`
var createIndexCardsCardbuild = `
var createIndexCardsCardBuild = `
CREATE INDEX ix_cards_build ON cards (card_build);
`
var createIndexCardsCardstep = `
CREATE UNIQUE INDEX ix_cards_step ON cards (card_step);
`
//
// 018_amend_table_cards.sql
//
var dropTableCards = `
DROP TABLE IF EXISTS cards;
`
var alterTableStepsAddColumnStepschema = `
ALTER TABLE steps
ADD COLUMN step_schema VARCHAR(2000) NOT NULL DEFAULT '';
`
var createNewTableCards = `
CREATE TABLE IF NOT EXISTS cards
(
card_id INTEGER PRIMARY KEY,
card_data BLOB,
FOREIGN KEY (card_id) REFERENCES steps (step_id) ON DELETE CASCADE
);
`

View file

@ -0,0 +1,16 @@
-- name: drop-table-cards
DROP TABLE IF EXISTS cards;
-- name: alter-table-steps-add-column-step_schema
ALTER TABLE steps
ADD COLUMN step_schema VARCHAR(2000) NOT NULL DEFAULT '';
-- name: create-new-table-cards
CREATE TABLE IF NOT EXISTS cards
(
card_id INTEGER PRIMARY KEY,
card_data BLOB,
FOREIGN KEY (card_id) REFERENCES steps (step_id) ON DELETE CASCADE
);

View file

@ -180,6 +180,22 @@ var migrations = []struct {
name: "create-index-cards-card_build",
stmt: createIndexCardsCardbuild,
},
{
name: "create-index-cards-card_step",
stmt: createIndexCardsCardstep,
},
{
name: "drop-table-cards",
stmt: dropTableCards,
},
{
name: "alter-table-steps-add-column-step_schema",
stmt: alterTableStepsAddColumnStepschema,
},
{
name: "create-new-table-cards",
stmt: createNewTableCards,
},
}
// Migrate performs the database migration. If the migration fails
@ -721,3 +737,29 @@ CREATE TABLE IF NOT EXISTS cards (
var createIndexCardsCardbuild = `
CREATE INDEX IF NOT EXISTS ix_cards_build ON cards (card_build);
`
var createIndexCardsCardstep = `
CREATE UNIQUE INDEX IF NOT EXISTS ix_cards_step ON cards (card_step);
`
//
// 019_amend_table_cards.sql
//
var dropTableCards = `
DROP TABLE IF EXISTS cards;
`
var alterTableStepsAddColumnStepschema = `
ALTER TABLE steps
ADD COLUMN step_schema VARCHAR(2000) NOT NULL DEFAULT '';
`
var createNewTableCards = `
CREATE TABLE IF NOT EXISTS cards
(
card_id SERIAL PRIMARY KEY,
card_data BYTEA,
FOREIGN KEY (card_id) REFERENCES steps (step_id) ON DELETE CASCADE
);
`

View file

@ -0,0 +1,16 @@
-- name: drop-table-cards
DROP TABLE IF EXISTS cards;
-- name: alter-table-steps-add-column-step_schema
ALTER TABLE steps
ADD COLUMN step_schema VARCHAR(2000) NOT NULL DEFAULT '';
-- name: create-new-table-cards
CREATE TABLE IF NOT EXISTS cards
(
card_id SERIAL PRIMARY KEY,
card_data BYTEA,
FOREIGN KEY (card_id) REFERENCES steps (step_id) ON DELETE CASCADE
);

View file

@ -180,6 +180,22 @@ var migrations = []struct {
name: "create-index-cards-card_build",
stmt: createIndexCardsCardbuild,
},
{
name: "create-index-cards-card_step",
stmt: createIndexCardsCardstep,
},
{
name: "drop-table-cards",
stmt: dropTableCards,
},
{
name: "alter-table-steps-add-column-step_schema",
stmt: alterTableStepsAddColumnStepschema,
},
{
name: "create-new-table-cards",
stmt: createNewTableCards,
},
}
// Migrate performs the database migration. If the migration fails
@ -723,3 +739,29 @@ CREATE TABLE IF NOT EXISTS cards (
var createIndexCardsCardbuild = `
CREATE INDEX IF NOT EXISTS ix_cards_build ON cards (card_build);
`
var createIndexCardsCardstep = `
CREATE UNIQUE INDEX IF NOT EXISTS ix_cards_step ON cards (card_step);
`
//
// 018_amend_table_cards.sql
//
var dropTableCards = `
DROP TABLE IF EXISTS cards;
`
var alterTableStepsAddColumnStepschema = `
ALTER TABLE steps
ADD COLUMN step_schema TEXT NOT NULL DEFAULT '';
`
var createNewTableCards = `
CREATE TABLE IF NOT EXISTS cards
(
card_id INTEGER PRIMARY KEY,
card_data BLOB,
FOREIGN KEY (card_id) REFERENCES steps (step_id) ON DELETE CASCADE
);
`

View file

@ -0,0 +1,16 @@
-- name: drop-table-cards
DROP TABLE IF EXISTS cards;
-- name: alter-table-steps-add-column-step_schema
ALTER TABLE steps
ADD COLUMN step_schema TEXT NOT NULL DEFAULT '';
-- name: create-new-table-cards
CREATE TABLE IF NOT EXISTS cards
(
card_id INTEGER PRIMARY KEY,
card_data BLOB,
FOREIGN KEY (card_id) REFERENCES steps (step_id) ON DELETE CASCADE
);

View file

@ -155,6 +155,7 @@ func scanRowStep(scanner db.Scanner, stage *core.Stage, step *nullStep) error {
&stepDepJSON,
&step.Image,
&step.Detached,
&step.Schema,
)
json.Unmarshal(depJSON, &stage.DependsOn)
json.Unmarshal(labJSON, &stage.Labels)

View file

@ -326,6 +326,7 @@ SELECT
,step_depends_on
,step_image
,step_detached
,step_schema
FROM stages
LEFT JOIN steps
ON stages.stage_id=steps.step_stage_id

View file

@ -37,6 +37,7 @@ type nullStep struct {
DependsOn types.JSONText
Image sql.NullString
Detached sql.NullBool
Schema sql.NullString
}
func (s *nullStep) value() *core.Step {
@ -58,6 +59,7 @@ func (s *nullStep) value() *core.Step {
DependsOn: dependsOn,
Image: s.Image.String,
Detached: s.Detached.Bool,
Schema: s.Schema.String,
}
return step

View file

@ -20,6 +20,7 @@ import (
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db"
"github.com/jmoiron/sqlx/types"
)
@ -41,6 +42,7 @@ func toParams(from *core.Step) map[string]interface{} {
"step_depends_on": encodeSlice(from.DependsOn),
"step_image": from.Image,
"step_detached": from.Detached,
"step_schema": from.Schema,
}
}
@ -68,6 +70,7 @@ func scanRow(scanner db.Scanner, dest *core.Step) error {
&depJSON,
&dest.Image,
&dest.Detached,
&dest.Schema,
)
json.Unmarshal(depJSON, &dest.DependsOn)
return err

View file

@ -159,6 +159,7 @@ SELECT
,step_depends_on
,step_image
,step_detached
,step_schema
`
const queryKey = queryBase + `
@ -191,6 +192,7 @@ SET
,step_depends_on = :step_depends_on
,step_image = :step_image
,step_detached = :step_detached
,step_schema = :step_schema
WHERE step_id = :step_id
AND step_version = :step_version_old
`
@ -210,6 +212,7 @@ INSERT INTO steps (
,step_depends_on
,step_image
,step_detached
,step_schema
) VALUES (
:step_stage_id
,:step_number
@ -224,6 +227,7 @@ INSERT INTO steps (
,:step_depends_on
,:step_image
,:step_detached
,:step_schema
)
`