Merge pull request #3149 from drone/feature/dron-101-cards

Feature/dron 101 cards
This commit is contained in:
Brad Rydzewski 2021-10-01 13:30:23 -04:00 committed by GitHub
commit 3c39e541bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1870 additions and 4 deletions

View file

@ -79,6 +79,7 @@ tasks:
- cmd: go test -count=1 github.com/drone/drone/store/batch
- cmd: go test -count=1 github.com/drone/drone/store/batch2
- cmd: go test -count=1 github.com/drone/drone/store/build
- cmd: go test -count=1 github.com/drone/drone/store/card
- cmd: go test -count=1 github.com/drone/drone/store/cron
- cmd: go test -count=1 github.com/drone/drone/store/logs
- cmd: go test -count=1 github.com/drone/drone/store/perm
@ -113,6 +114,7 @@ tasks:
- cmd: go test -count=1 github.com/drone/drone/store/batch
- cmd: go test -count=1 github.com/drone/drone/store/batch2
- cmd: go test -count=1 github.com/drone/drone/store/build
- cmd: go test -count=1 github.com/drone/drone/store/card
- cmd: go test -count=1 github.com/drone/drone/store/cron
- cmd: go test -count=1 github.com/drone/drone/store/logs
- cmd: go test -count=1 github.com/drone/drone/store/perm

View file

@ -21,6 +21,7 @@ import (
"github.com/drone/drone/store/batch"
"github.com/drone/drone/store/batch2"
"github.com/drone/drone/store/build"
"github.com/drone/drone/store/card"
"github.com/drone/drone/store/cron"
"github.com/drone/drone/store/logs"
"github.com/drone/drone/store/perm"
@ -50,6 +51,7 @@ var storeSet = wire.NewSet(
provideBatchStore,
// batch.New,
cron.New,
card.New,
perm.New,
secret.New,
global.New,

View file

@ -1,6 +1,6 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:generate wire
//+build !wireinject
package main
@ -20,6 +20,7 @@ import (
"github.com/drone/drone/service/token"
"github.com/drone/drone/service/transfer"
"github.com/drone/drone/service/user"
"github.com/drone/drone/store/card"
"github.com/drone/drone/store/cron"
"github.com/drone/drone/store/perm"
"github.com/drone/drone/store/secret"
@ -86,6 +87,7 @@ func InitializeApplication(config2 config.Config) (application, error) {
secretService := provideSecretPlugin(config2)
registryService := provideRegistryPlugin(config2)
runner := provideRunner(buildManager, secretService, registryService, config2)
cardStore := card.New(db)
hookService := provideHookService(client, renewer, config2)
licenseService := license.NewService(userStore, repositoryStore, buildStore, coreLicense)
organizationService := provideOrgService(client, renewer)
@ -99,7 +101,7 @@ func InitializeApplication(config2 config.Config) (application, error) {
syncer := provideSyncer(repositoryService, repositoryStore, userStore, batcher, config2)
transferer := transfer.New(repositoryStore, permStore)
userService := user.New(client, renewer)
server := api.New(buildStore, commitService, cronStore, corePubsub, globalSecretStore, hookService, logStore, coreLicense, licenseService, organizationService, permStore, repositoryStore, repositoryService, scheduler, secretStore, stageStore, stepStore, statusService, session, logStream, syncer, system, templateStore, transferer, triggerer, userStore, userService, webhookSender)
server := api.New(buildStore, commitService, cardStore, cronStore, corePubsub, globalSecretStore, hookService, logStore, coreLicense, licenseService, organizationService, permStore, repositoryStore, repositoryService, scheduler, secretStore, stageStore, stepStore, statusService, session, logStream, syncer, system, templateStore, transferer, triggerer, userStore, userService, webhookSender)
admissionService := provideAdmissionPlugin(client, organizationService, userService, config2)
hookParser := parser.New(client)
coreLinker := linker.New(client)

64
core/card.go Normal file
View file

@ -0,0 +1,64 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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
}
// 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
}
}

View file

@ -23,6 +23,7 @@ import (
"github.com/drone/drone/handler/api/auth"
"github.com/drone/drone/handler/api/badge"
globalbuilds "github.com/drone/drone/handler/api/builds"
"github.com/drone/drone/handler/api/card"
"github.com/drone/drone/handler/api/ccmenu"
"github.com/drone/drone/handler/api/events"
"github.com/drone/drone/handler/api/queue"
@ -63,6 +64,7 @@ var corsOpts = cors.Options{
func New(
builds core.BuildStore,
commits core.CommitService,
card core.CardStore,
cron core.CronStore,
events core.Pubsub,
globals core.GlobalSecretStore,
@ -92,6 +94,7 @@ func New(
) Server {
return Server{
Builds: builds,
Card: card,
Cron: cron,
Commits: commits,
Events: events,
@ -125,6 +128,7 @@ func New(
// Server is a http.Handler which exposes drone functionality over HTTP.
type Server struct {
Builds core.BuildStore
Card core.CardStore
Cron core.CronStore
Commits core.CommitService
Events core.Pubsub
@ -285,6 +289,18 @@ func (s Server) Handler() http.Handler {
acl.CheckAdminAccess(),
).Delete("/{member}", collabs.HandleDelete(s.Users, s.Repos, s.Perms))
})
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))
r.With(
acl.CheckAdminAccess(),
).Delete("/{build}/{stage}/{step}", card.HandleDelete(s.Builds, s.Card, s.Stages, s.Steps, s.Repos))
})
})
})

113
handler/api/card/create.go Normal file
View file

@ -0,0 +1,113 @@
// 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"
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/go-chi/chi"
)
type cardInput struct {
Schema string `json:"schema"`
Data json.RawMessage `json:"data"`
}
// HandleCreate returns an http.HandlerFunc that processes http
// requests to create a new card.
func HandleCreate(
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
}
in := new(cardInput)
err = json.NewDecoder(r.Body).Decode(in)
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
}
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)),
)
err = cardStore.Create(r.Context(), c, data)
if err != nil {
render.InternalError(w, err)
return
}
render.JSON(w, c, 200)
}
}

View file

@ -0,0 +1,174 @@
// 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"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/drone/drone/core"
"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"
)
var (
dummyRepo = &core.Repository{
ID: 1,
UserID: 1,
Slug: "octocat/hello-world",
}
dummyBuild = &core.Build{
ID: 1,
RepoID: 1,
Number: 1,
}
dummyStage = &core.Stage{
ID: 1,
BuildID: 1,
}
dummyStep = &core.Step{
ID: 1,
StageID: 1,
}
dummyCreateCard = &core.Card{
Schema: "https://myschema.com",
}
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) {
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().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(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("POST", "/", in)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleCreate(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)
}
}
func TestHandleCreate_BadRequest(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
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")
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/", nil)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleCreate(nil, nil, nil, nil, nil).ServeHTTP(w, r)
got, want := &errors.Error{}, &errors.Error{Message: "EOF"}
json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
func TestHandleCreate_CreateError(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().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(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),
)
HandleCreate(build, card, stage, step, repos).ServeHTTP(w, r)
if got, want := w.Code, http.StatusInternalServerError; 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

@ -0,0 +1,86 @@
// 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 (
"strconv"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"net/http"
"github.com/go-chi/chi"
)
// HandleDelete returns an http.HandlerFunc that processes http
// requests to delete a card.
func HandleDelete(
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
}
err = cardStore.Delete(r.Context(), card.Id)
if err != nil {
render.InternalError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
}

View file

@ -0,0 +1,150 @@
// 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 (
"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 TestHandleDelete(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().Delete(gomock.Any(), dummyCard.Id).Return(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")
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", "/", nil)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleDelete(build, card, stage, step, repos).ServeHTTP(w, r)
if got, want := w.Code, http.StatusNoContent; want != got {
t.Errorf("Want response code %d, got %d", want, got)
}
}
func TestHandleDelete_CardNotFound(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(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")
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", "/", nil)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleDelete(build, card, stage, step, 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)
}
}
func TestHandleDelete_DeleteError(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().Delete(gomock.Any(), dummyCard.Id).Return(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")
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", "/", nil)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleDelete(build, card, stage, step, repos).ServeHTTP(w, r)
if got, want := w.Code, http.StatusInternalServerError; 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)
}
}

79
handler/api/card/find.go Normal file
View file

@ -0,0 +1,79 @@
// 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"
)
// HandleFind returns an http.HandlerFunc that writes a json-encoded
func HandleFind(
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
}
render.JSON(w, card, 200)
}
}

View file

@ -0,0 +1,55 @@
// 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

@ -0,0 +1,97 @@
// 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

@ -0,0 +1,87 @@
// 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

@ -0,0 +1,63 @@
// 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

@ -0,0 +1,111 @@
// 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 TestHandleFind(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)
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),
)
HandleFind(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)
}
}
func TestHandleFind_CardNotFound(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(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),
)
HandleFind(build, card, stage, step, 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)
}
}

76
handler/api/card/none.go Normal file
View file

@ -0,0 +1,76 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build oss
package card
import (
"net/http"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
)
var notImplemented = func(w http.ResponseWriter, r *http.Request) {
render.NotImplemented(w, render.ErrNotImplemented)
}
func HandleCreate(
buildStore core.BuildStore,
cardStore core.CardStore,
stageStore core.StageStore,
stepStore core.StepStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return notImplemented
}
func HandleDelete(
buildStore core.BuildStore,
cardStore core.CardStore,
stageStore core.StageStore,
stepStore core.StepStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return notImplemented
}
func HandleFind(
buildStore core.BuildStore,
cardStore core.CardStore,
stageStore core.StageStore,
stepStore core.StepStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return notImplemented
}
func HandleFindAll(
buildStore core.BuildStore,
cardStore core.CardStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return notImplemented
}
func HandleFindData(
buildStore core.BuildStore,
cardStore core.CardStore,
stageStore core.StageStore,
stepStore core.StepStore,
repoStore core.RepositoryStore,
) http.HandlerFunc {
return notImplemented
}

View file

@ -6,4 +6,4 @@
package mock
//go:generate mockgen -package=mock -destination=mock_gen.go github.com/drone/drone/core Pubsub,Canceler,ConvertService,ValidateService,NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,GlobalSecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Transferer,Triggerer,Syncer,LogStream,WebhookSender,LicenseService,TemplateStore
//go:generate mockgen -package=mock -destination=mock_gen.go github.com/drone/drone/core Pubsub,Canceler,ConvertService,ValidateService,NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,GlobalSecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Transferer,Triggerer,Syncer,LogStream,WebhookSender,LicenseService,TemplateStore,CardStore

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/drone/drone/core (interfaces: Pubsub,Canceler,ConvertService,ValidateService,NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,GlobalSecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Transferer,Triggerer,Syncer,LogStream,WebhookSender,LicenseService,TemplateStore)
// Source: github.com/drone/drone/core (interfaces: Pubsub,Canceler,ConvertService,ValidateService,NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,GlobalSecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Transferer,Triggerer,Syncer,LogStream,WebhookSender,LicenseService,TemplateStore,CardStore)
// Package mock is a generated GoMock package.
package mock
@ -2894,3 +2894,99 @@ func (mr *MockTemplateStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTemplateStore)(nil).Update), arg0, arg1)
}
// MockCardStore is a mock of CardStore interface.
type MockCardStore struct {
ctrl *gomock.Controller
recorder *MockCardStoreMockRecorder
}
// MockCardStoreMockRecorder is the mock recorder for MockCardStore.
type MockCardStoreMockRecorder struct {
mock *MockCardStore
}
// NewMockCardStore creates a new mock instance.
func NewMockCardStore(ctrl *gomock.Controller) *MockCardStore {
mock := &MockCardStore{ctrl: ctrl}
mock.recorder = &MockCardStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCardStore) EXPECT() *MockCardStoreMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockCardStore) Create(arg0 context.Context, arg1 *core.Card, arg2 io.ReadCloser) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
func (mr *MockCardStoreMockRecorder) Create(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCardStore)(nil).Create), arg0, arg1, arg2)
}
// Delete mocks base method.
func (m *MockCardStore) Delete(arg0 context.Context, arg1 int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockCardStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCardStore)(nil).Delete), arg0, arg1)
}
// Find mocks base method.
func (m *MockCardStore) Find(arg0 context.Context, arg1 int64) (*core.Card, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", arg0, arg1)
ret0, _ := ret[0].(*core.Card)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Find indicates an expected call of Find.
func (mr *MockCardStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
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) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByBuild", arg0, arg1)
ret0, _ := ret[0].([]*core.Card)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByBuild indicates an expected call of FindByBuild.
func (mr *MockCardStoreMockRecorder) FindByBuild(arg0, arg1 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)
}

201
store/card/card.go Normal file
View file

@ -0,0 +1,201 @@
// 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"
"io"
"io/ioutil"
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db"
)
// New returns a new card database store.
func New(db *db.DB) core.CardStore {
return &cardStore{
db: db,
}
}
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
}
func (c cardStore) Find(ctx context.Context, step int64) (*core.Card, error) {
out := &core.Card{Step: 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 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)
}
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
}
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()
return err
})
}
func (c *cardStore) createPostgres(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
}
stmt, args, err := binder.BindNamed(stmtInsertPostgres, params)
if err != nil {
return err
}
return execer.QueryRow(stmt, args...).Scan(&card.Id)
})
}
func (c cardStore) Delete(ctx context.Context, id int64) error {
return c.db.Lock(func(execer db.Execer, binder db.Binder) error {
params := map[string]interface{}{
"card_id": id,
}
stmt, args, err := binder.BindNamed(stmtDelete, params)
if err != nil {
return err
}
_, err = execer.Exec(stmt, args...)
return err
})
}
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 + `
FROM cards
WHERE card_id = :card_id
LIMIT 1
`
const stmtInsert = `
INSERT INTO cards (
card_build
,card_stage
,card_step
,card_schema
,card_data
) VALUES (
:card_build
,:card_stage
,:card_step
,:card_schema
,:card_data
)
`
const stmtDelete = `
DELETE FROM cards
WHERE card_id = :card_id
`
const stmtInsertPostgres = stmtInsert + `
RETURNING card_id
`

51
store/card/card_oss.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build oss
package card
import (
"context"
"io"
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db"
)
func New(db *db.DB) core.CardStore {
return new(noop)
}
type noop struct{}
func (noop) FindByBuild(ctx context.Context, build int64) ([]*core.Card, 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 {
return nil
}
func (noop) Delete(ctx context.Context, id int64) error {
return nil
}

133
store/card/card_test.go Normal file
View file

@ -0,0 +1,133 @@
package card
import (
"bytes"
"context"
"database/sql"
"io/ioutil"
"testing"
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db/dbtest"
)
var noContext = context.TODO()
func TestCard(t *testing.T) {
conn, err := dbtest.Connect()
if err != nil {
t.Error(err)
return
}
defer func() {
dbtest.Reset(conn)
dbtest.Disconnect(conn)
}()
store := New(conn).(*cardStore)
t.Run("Create", testCardCreate(store))
}
func testCard(item *core.Card) 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)
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) {
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)
if err != nil {
t.Error(err)
} else {
data, err := ioutil.ReadAll(r)
if err != nil {
t.Error(err)
return
}
if got, want := string(data), "{\"type\": \"AdaptiveCard\"}"; got != want {
t.Errorf("Want card data output stream %q, got %q", want, got)
}
}
}
}
func testCardDelete(store *cardStore) func(t *testing.T) {
return func(t *testing.T) {
card, err := store.Find(noContext, 3)
if err != nil {
t.Error(err)
return
}
err = store.Delete(noContext, card.Id)
if err != nil {
t.Error(err)
return
}
_, err = store.Find(noContext, card.Step)
if got, want := sql.ErrNoRows, err; got != want {
t.Errorf("Want sql.ErrNoRows, got %v", got)
return
}
}
}

79
store/card/scan.go Normal file
View file

@ -0,0 +1,79 @@
// 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 (
"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) {
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,
}, 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 {
err := scanner.Scan(
&dst.Id,
&dst.Build,
&dst.Stage,
&dst.Step,
&dst.Schema,
)
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

@ -180,6 +180,14 @@ var migrations = []struct {
name: "alter-table-steps-add-column-step-detached",
stmt: alterTableStepsAddColumnStepDetached,
},
{
name: "create-table-cards",
stmt: createTableCards,
},
{
name: "create-index-cards-card_build",
stmt: createIndexCardsCardbuild,
},
}
// Migrate performs the database migration. If the migration fails
@ -722,3 +730,22 @@ ALTER TABLE steps ADD COLUMN step_image VARCHAR(1000) NOT NULL DEFAULT '';
var alterTableStepsAddColumnStepDetached = `
ALTER TABLE steps ADD COLUMN step_detached BOOLEAN NOT NULL DEFAULT FALSE;
`
//
// 017_create_table_cards.sql
//
var createTableCards = `
CREATE TABLE IF NOT EXISTS cards (
card_id INTEGER PRIMARY KEY AUTO_INCREMENT
,card_build INTEGER
,card_stage INTEGER
,card_step INTEGER
,card_schema TEXT
,card_data TEXT
);
`
var createIndexCardsCardbuild = `
CREATE INDEX ix_cards_build ON cards (card_build);
`

View file

@ -0,0 +1,16 @@
-- name: create-table-cards
CREATE TABLE IF NOT EXISTS cards (
card_id INTEGER PRIMARY KEY AUTO_INCREMENT
,card_build INTEGER
,card_stage INTEGER
,card_step INTEGER
,card_schema TEXT
,card_data TEXT
);
-- name: create-index-cards-card-build
CREATE INDEX ix_cards_build ON cards (card_build);
-- name: create-index-cards-card_step
CREATE UNIQUE INDEX ix_cards_step ON cards (card_step);

View file

@ -172,6 +172,14 @@ var migrations = []struct {
name: "alter-table-steps-add-column-step-detached",
stmt: alterTableStepsAddColumnStepDetached,
},
{
name: "create-table-cards",
stmt: createTableCards,
},
{
name: "create-index-cards-card_build",
stmt: createIndexCardsCardbuild,
},
}
// Migrate performs the database migration. If the migration fails
@ -694,3 +702,22 @@ ALTER TABLE steps ADD COLUMN step_image VARCHAR(1000) NOT NULL DEFAULT '';
var alterTableStepsAddColumnStepDetached = `
ALTER TABLE steps ADD COLUMN step_detached BOOLEAN NOT NULL DEFAULT FALSE;
`
//
// 018_create_table_cards.sql
//
var createTableCards = `
CREATE TABLE IF NOT EXISTS cards (
card_id SERIAL PRIMARY KEY
,card_build INTEGER
,card_stage INTEGER
,card_step INTEGER
,card_schema TEXT
,card_data TEXT
);
`
var createIndexCardsCardbuild = `
CREATE INDEX IF NOT EXISTS ix_cards_build ON cards (card_build);
`

View file

@ -0,0 +1,16 @@
-- name: create-table-cards
CREATE TABLE IF NOT EXISTS cards (
card_id SERIAL PRIMARY KEY
,card_build INTEGER
,card_stage INTEGER
,card_step INTEGER
,card_schema TEXT
,card_data TEXT
);
-- name: create-index-cards-card_build
CREATE INDEX IF NOT EXISTS ix_cards_build ON cards (card_build);
-- name: create-index-cards-card_step
CREATE UNIQUE INDEX IF NOT EXISTS ix_cards_step ON cards (card_step);

View file

@ -172,6 +172,14 @@ var migrations = []struct {
name: "alter-table-steps-add-column-step-detached",
stmt: alterTableStepsAddColumnStepDetached,
},
{
name: "create-table-cards",
stmt: createTableCards,
},
{
name: "create-index-cards-card_build",
stmt: createIndexCardsCardbuild,
},
}
// Migrate performs the database migration. If the migration fails
@ -696,3 +704,22 @@ ALTER TABLE steps ADD COLUMN step_image TEXT NOT NULL DEFAULT '';
var alterTableStepsAddColumnStepDetached = `
ALTER TABLE steps ADD COLUMN step_detached BOOLEAN NOT NULL DEFAULT FALSE;
`
//
// 017_create_table_cards.sql
//
var createTableCards = `
CREATE TABLE IF NOT EXISTS cards (
card_id INTEGER PRIMARY KEY AUTOINCREMENT
,card_build INTEGER
,card_stage INTEGER
,card_step INTEGER
,card_schema TEXT
,card_data TEXT
);
`
var createIndexCardsCardbuild = `
CREATE INDEX IF NOT EXISTS ix_cards_build ON cards (card_build);
`

View file

@ -0,0 +1,16 @@
-- name: create-table-cards
CREATE TABLE IF NOT EXISTS cards (
card_id INTEGER PRIMARY KEY AUTOINCREMENT
,card_build INTEGER
,card_stage INTEGER
,card_step INTEGER
,card_schema TEXT
,card_data TEXT
);
-- name: create-index-cards-card_build
CREATE INDEX IF NOT EXISTS ix_cards_build ON cards (card_build);
-- name: create-index-cards-card_step
CREATE UNIQUE INDEX IF NOT EXISTS ix_cards_step ON cards (card_step);