From 85fd11c42115d09e2541c6235b4c1da5e182db2c Mon Sep 17 00:00:00 2001 From: Eoin McAfee Date: Fri, 24 Sep 2021 10:06:26 +0100 Subject: [PATCH] card api and db work to support new cards feature --- Taskfile.yml | 2 + cmd/drone-server/inject_store.go | 2 + cmd/drone-server/wire_gen.go | 6 +- core/card.go | 89 +++++++ go.mod | 2 +- go.sum | 2 + handler/api/acl/check.go | 41 ++++ handler/api/api.go | 19 ++ handler/api/card/create.go | 108 +++++++++ handler/api/card/create_test.go | 224 ++++++++++++++++++ handler/api/card/delete.go | 86 +++++++ handler/api/card/delete_test.go | 150 ++++++++++++ handler/api/card/find.go | 79 ++++++ handler/api/card/find_all.go | 55 +++++ handler/api/card/find_all_test.go | 97 ++++++++ handler/api/card/find_data.go | 87 +++++++ handler/api/card/find_data_test.go | 63 +++++ handler/api/card/find_test.go | 111 +++++++++ handler/api/card/none.go | 76 ++++++ mock/mock.go | 2 +- mock/mock_gen.go | 98 +++++++- store/card/card.go | 193 +++++++++++++++ store/card/card_oss.go | 51 ++++ store/card/card_test.go | 130 ++++++++++ store/card/scan.go | 79 ++++++ store/shared/migrate/mysql/ddl_gen.go | 27 +++ .../mysql/files/017_create_table_cards.sql | 16 ++ store/shared/migrate/postgres/ddl_gen.go | 27 +++ .../postgres/files/018_create_table_cards.sql | 16 ++ store/shared/migrate/sqlite/ddl_gen.go | 27 +++ .../sqlite/files/017_create_table_cards.sql | 16 ++ 31 files changed, 1976 insertions(+), 5 deletions(-) create mode 100644 core/card.go create mode 100644 handler/api/card/create.go create mode 100644 handler/api/card/create_test.go create mode 100644 handler/api/card/delete.go create mode 100644 handler/api/card/delete_test.go create mode 100644 handler/api/card/find.go create mode 100644 handler/api/card/find_all.go create mode 100644 handler/api/card/find_all_test.go create mode 100644 handler/api/card/find_data.go create mode 100644 handler/api/card/find_data_test.go create mode 100644 handler/api/card/find_test.go create mode 100644 handler/api/card/none.go create mode 100644 store/card/card.go create mode 100644 store/card/card_oss.go create mode 100644 store/card/card_test.go create mode 100644 store/card/scan.go create mode 100644 store/shared/migrate/mysql/files/017_create_table_cards.sql create mode 100644 store/shared/migrate/postgres/files/018_create_table_cards.sql create mode 100644 store/shared/migrate/sqlite/files/017_create_table_cards.sql diff --git a/Taskfile.yml b/Taskfile.yml index 200940ea..858f034c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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 diff --git a/cmd/drone-server/inject_store.go b/cmd/drone-server/inject_store.go index 0e06e9b0..23a6dcf1 100644 --- a/cmd/drone-server/inject_store.go +++ b/cmd/drone-server/inject_store.go @@ -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, diff --git a/cmd/drone-server/wire_gen.go b/cmd/drone-server/wire_gen.go index 9502b10c..ad246dbf 100644 --- a/cmd/drone-server/wire_gen.go +++ b/cmd/drone-server/wire_gen.go @@ -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) diff --git a/core/card.go b/core/card.go new file mode 100644 index 00000000..0547df16 --- /dev/null +++ b/core/card.go @@ -0,0 +1,89 @@ +// 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") + errCardDataInvalid = errors.New("No Card Data 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 CreateCard 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"` + Data []byte `json:"data,omitempty"` +} + +type CardData struct { + Id int64 `json:"id,omitempty"` + Data []byte `json:"card_data"` +} + +// CardStore manages repository cards. +type CardStore interface { + FindCardByBuild(ctx context.Context, build int64) ([]*Card, error) + FindCard(ctx context.Context, step int64) (*Card, error) + FindCardData(ctx context.Context, id int64) (io.ReadCloser, error) + CreateCard(ctx context.Context, card *CreateCard) error + DeleteCard(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 + } +} + +func (c *CreateCard) Validate() error { + switch { + case c.Step == 0: + return errCardStepInvalid + case c.Build == 0: + return errCardBuildInvalid + case len(c.Schema) == 0: + return errCardSchemaInvalid + case len(c.Data) == 0: + return errCardDataInvalid + default: + return nil + } +} diff --git a/go.mod b/go.mod index dcff6e23..67ad3533 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/aws/aws-sdk-go v1.37.3 github.com/codegangsta/negroni v1.0.0 // indirect github.com/coreos/go-semver v0.2.0 - github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866 + github.com/dchest/authcookie v0.0.0-20190824115100-f900d2294c8e github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 github.com/drone/drone-go v1.4.1-0.20201109202657-b9e58bbbcf27 github.com/drone/drone-runtime v1.1.1-0.20200623162453-61e33e2cab5d diff --git a/go.sum b/go.sum index 25cbfedb..200d3faf 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866 h1:98WJ4YCdjmB7uyrdT3P4A2Oa1hiRPKoa/0zInG6UnfQ= github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866/go.mod h1:x7AK2h2QzaXVEFi1tbMYMDuvHcCEr1QdMDrg3hkW24Q= +github.com/dchest/authcookie v0.0.0-20190824115100-f900d2294c8e h1:xizeG5ksKSdyNaom2//2Bow4hLWqXkCql36nrL9iEUI= +github.com/dchest/authcookie v0.0.0-20190824115100-f900d2294c8e/go.mod h1:x7AK2h2QzaXVEFi1tbMYMDuvHcCEr1QdMDrg3hkW24Q= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= diff --git a/handler/api/acl/check.go b/handler/api/acl/check.go index 2eaf6f7e..7a5f0fe9 100644 --- a/handler/api/acl/check.go +++ b/handler/api/acl/check.go @@ -15,7 +15,10 @@ package acl import ( + "github.com/dchest/authcookie" "net/http" + "os" + "strings" "github.com/drone/drone/core" "github.com/drone/drone/handler/api/errors" @@ -48,6 +51,36 @@ func CheckAdminAccess() func(http.Handler) http.Handler { return CheckAccess(true, true, true) } +func CheckInternalAccess() func(http.Handler) http.Handler { + return checkIAccess() +} + +// CheckIAccess returns an http.Handler middleware that authorizes only +// allows plugins to communicate with the server +func checkIAccess() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + owner = chi.URLParam(r, "owner") + name = chi.URLParam(r, "name") + ) + log := logger.FromRequest(r). + WithField("namespace", owner). + WithField("name", name) + + secret := []byte(os.Getenv("DRONE_INTERNAL_AUTH_SECRET")) + login := authcookie.Login(extractToken(r), secret) + if login != "" { + log.Debug("api: access granted") + next.ServeHTTP(w, r) + } + render.Unauthorized(w, errors.ErrUnauthorized) + log.Debugln("api: authentication required for access") + return + }) + } +} + // CheckAccess returns an http.Handler middleware that authorizes only // authenticated users with the required read, write or admin access // permissions to the requested repository resource. @@ -144,3 +177,11 @@ func CheckAccess(read, write, admin bool) func(http.Handler) http.Handler { }) } } + +func extractToken(r *http.Request) string { + bearer := r.Header.Get("Authorization") + if bearer == "" { + bearer = r.FormValue("access_token") + } + return strings.TrimPrefix(bearer, "Bearer ") +} diff --git a/handler/api/api.go b/handler/api/api.go index c33f2e62..3727076c 100644 --- a/handler/api/api.go +++ b/handler/api/api.go @@ -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 @@ -166,6 +170,11 @@ func (s Server) Handler() http.Handler { cors := cors.New(corsOpts) r.Use(cors.Handler) + r.Route("/internal/{owner}/{name}", func(r chi.Router) { + r.With( + acl.CheckInternalAccess(), + ).Post("/cards/{build}/{stage}/{step}", card.HandleCreate(s.Builds, s.Card, s.Stages, s.Steps, s.Repos)) + }) r.Route("/repos", func(r chi.Router) { // temporary workaround to enable private mode // for the drone server. @@ -285,6 +294,16 @@ 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.AuthorizeAdmin, + acl.CheckAdminAccess(), + ).Delete("/{build}/{stage}/{step}", card.HandleDelete(s.Builds, s.Card, s.Stages, s.Steps, s.Repos)) + }) }) }) diff --git a/handler/api/card/create.go b/handler/api/card/create.go new file mode 100644 index 00000000..7c9aa1f9 --- /dev/null +++ b/handler/api/card/create.go @@ -0,0 +1,108 @@ +// 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 ( + "encoding/json" + "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.CreateCard{ + Build: build.ID, + Stage: stage.ID, + Step: step.ID, + Schema: in.Schema, + Data: in.Data, + } + + err = c.Validate() + if err != nil { + render.BadRequest(w, err) + return + } + + err = cardStore.CreateCard(r.Context(), c) + if err != nil { + render.InternalError(w, err) + return + } + + render.JSON(w, c, 200) + } +} diff --git a/handler/api/card/create_test.go b/handler/api/card/create_test.go new file mode 100644 index 00000000..77e81456 --- /dev/null +++ b/handler/api/card/create_test.go @@ -0,0 +1,224 @@ +// 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.CreateCard{ + Schema: "https://myschema.com", + Data: "{\"type\": \"AdaptiveCard\"}", + } + dummyCard = &core.Card{ + Id: 1, + Build: 1, + Stage: 1, + Step: 1, + Schema: "https://myschema.com", + } + dummyCardNoData = &core.CreateCard{ + Schema: "https://myschema.com", + } + dummyCardList = []*core.Card{ + dummyCard, + } + dummyCardData = ioutil.NopCloser( + bytes.NewBuffer([]byte(dummyCreateCard.Data)), + ) +) + +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().CreateCard(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_ValidationErrorData(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) + + 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(dummyCardNoData) + + 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.StatusBadRequest; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := &errors.Error{}, &errors.Error{Message: "No Card Data Has Been Provided"} + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} + +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().CreateCard(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) + } +} diff --git a/handler/api/card/delete.go b/handler/api/card/delete.go new file mode 100644 index 00000000..1779f5c5 --- /dev/null +++ b/handler/api/card/delete.go @@ -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.FindCard(r.Context(), step.ID) + if err != nil { + render.NotFound(w, err) + return + } + err = cardStore.DeleteCard(r.Context(), card.Id) + if err != nil { + render.InternalError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/handler/api/card/delete_test.go b/handler/api/card/delete_test.go new file mode 100644 index 00000000..0194816d --- /dev/null +++ b/handler/api/card/delete_test.go @@ -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().FindCard(gomock.Any(), dummyStep.ID).Return(dummyCard, nil) + card.EXPECT().DeleteCard(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().FindCard(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().FindCard(gomock.Any(), dummyStep.ID).Return(dummyCard, nil) + card.EXPECT().DeleteCard(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) + } +} diff --git a/handler/api/card/find.go b/handler/api/card/find.go new file mode 100644 index 00000000..d8fda447 --- /dev/null +++ b/handler/api/card/find.go @@ -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.FindCard(r.Context(), step.ID) + if err != nil { + render.NotFound(w, err) + return + } + render.JSON(w, card, 200) + } +} diff --git a/handler/api/card/find_all.go b/handler/api/card/find_all.go new file mode 100644 index 00000000..10e2fa67 --- /dev/null +++ b/handler/api/card/find_all.go @@ -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.FindCardByBuild(r.Context(), build.ID) + if err != nil { + render.NotFound(w, err) + return + } + render.JSON(w, list, 200) + } +} diff --git a/handler/api/card/find_all_test.go b/handler/api/card/find_all_test.go new file mode 100644 index 00000000..b0bdf08b --- /dev/null +++ b/handler/api/card/find_all_test.go @@ -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().FindCardByBuild(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().FindCardByBuild(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) + } +} diff --git a/handler/api/card/find_data.go b/handler/api/card/find_data.go new file mode 100644 index 00000000..56f34efa --- /dev/null +++ b/handler/api/card/find_data.go @@ -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.FindCard(r.Context(), step.ID) + if err != nil { + render.NotFound(w, err) + return + } + cardData, err := cardStore.FindCardData(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() + } +} diff --git a/handler/api/card/find_data_test.go b/handler/api/card/find_data_test.go new file mode 100644 index 00000000..2ea749b0 --- /dev/null +++ b/handler/api/card/find_data_test.go @@ -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().FindCard(gomock.Any(), dummyStep.ID).Return(dummyCard, nil) + card.EXPECT().FindCardData(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) + } +} diff --git a/handler/api/card/find_test.go b/handler/api/card/find_test.go new file mode 100644 index 00000000..78b2c412 --- /dev/null +++ b/handler/api/card/find_test.go @@ -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().FindCard(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().FindCard(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) + } +} diff --git a/handler/api/card/none.go b/handler/api/card/none.go new file mode 100644 index 00000000..5b843468 --- /dev/null +++ b/handler/api/card/none.go @@ -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 +} diff --git a/mock/mock.go b/mock/mock.go index 9528323e..f91c1e5d 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -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 diff --git a/mock/mock_gen.go b/mock/mock_gen.go index b630a3dd..224d6474 100644 --- a/mock/mock_gen.go +++ b/mock/mock_gen.go @@ -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 @@ -2879,3 +2879,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 +} + +// CreateCard mocks base method. +func (m *MockCardStore) CreateCard(arg0 context.Context, arg1 *core.CreateCard) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCard", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateCard indicates an expected call of CreateCard. +func (mr *MockCardStoreMockRecorder) CreateCard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCard", reflect.TypeOf((*MockCardStore)(nil).CreateCard), arg0, arg1) +} + +// DeleteCard mocks base method. +func (m *MockCardStore) DeleteCard(arg0 context.Context, arg1 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCard", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCard indicates an expected call of DeleteCard. +func (mr *MockCardStoreMockRecorder) DeleteCard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCard", reflect.TypeOf((*MockCardStore)(nil).DeleteCard), arg0, arg1) +} + +// FindCard mocks base method. +func (m *MockCardStore) FindCard(arg0 context.Context, arg1 int64) (*core.Card, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindCard", arg0, arg1) + ret0, _ := ret[0].(*core.Card) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindCard indicates an expected call of FindCard. +func (mr *MockCardStoreMockRecorder) FindCard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindCard", reflect.TypeOf((*MockCardStore)(nil).FindCard), arg0, arg1) +} + +// FindCardByBuild mocks base method. +func (m *MockCardStore) FindCardByBuild(arg0 context.Context, arg1 int64) ([]*core.Card, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindCardByBuild", arg0, arg1) + ret0, _ := ret[0].([]*core.Card) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindCardByBuild indicates an expected call of FindCardByBuild. +func (mr *MockCardStoreMockRecorder) FindCardByBuild(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindCardByBuild", reflect.TypeOf((*MockCardStore)(nil).FindCardByBuild), arg0, arg1) +} + +// FindCardData mocks base method. +func (m *MockCardStore) FindCardData(arg0 context.Context, arg1 int64) (io.Reader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindCardData", arg0, arg1) + ret0, _ := ret[0].(io.Reader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindCardData indicates an expected call of FindCardData. +func (mr *MockCardStoreMockRecorder) FindCardData(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindCardData", reflect.TypeOf((*MockCardStore)(nil).FindCardData), arg0, arg1) +} diff --git a/store/card/card.go b/store/card/card.go new file mode 100644 index 00000000..52470b38 --- /dev/null +++ b/store/card/card.go @@ -0,0 +1,193 @@ +// 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) FindCardByBuild(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) FindCard(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) FindCardData(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) CreateCard(ctx context.Context, card *core.CreateCard) error { + if c.db.Driver() == db.Postgres { + return c.createPostgres(ctx, card) + } + return c.create(ctx, card) +} + +func (c *cardStore) create(ctx context.Context, card *core.CreateCard) error { + return c.db.Lock(func(execer db.Execer, binder db.Binder) error { + params, err := toSaveCardParams(card) + 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.CreateCard) error { + return c.db.Lock(func(execer db.Execer, binder db.Binder) error { + params, err := toSaveCardParams(card) + 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) DeleteCard(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 +` diff --git a/store/card/card_oss.go b/store/card/card_oss.go new file mode 100644 index 00000000..5ed3ce97 --- /dev/null +++ b/store/card/card_oss.go @@ -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) FindCardByBuild(ctx context.Context, build int64) ([]*core.Card, error) { + return nil, nil +} + +func (noop) FindCard(ctx context.Context, step int64) (*core.Card, error) { + return nil, nil +} + +func (noop) FindCardData(ctx context.Context, id int64) (io.Reader, error) { + return nil, nil +} + +func (noop) CreateCard(ctx context.Context, card *core.CreateCard) error { + return nil +} + +func (noop) DeleteCard(ctx context.Context, id int64) error { + return nil +} diff --git a/store/card/card_test.go b/store/card/card_test.go new file mode 100644 index 00000000..8aa22e5d --- /dev/null +++ b/store/card/card_test.go @@ -0,0 +1,130 @@ +package card + +import ( + "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.CreateCard{ + Id: 1, + Build: 1, + Stage: 2, + Step: 3, + Schema: "https://myschema.com", + Data: "{\"type\": \"AdaptiveCard\"}", + } + err := store.CreateCard(noContext, item) + 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("FindCard", testFindCard(store)) + t.Run("FindCardData", testFindCardData(store)) + t.Run("Delete", testCardDelete(store)) + } +} + +func testFindCardByBuild(card *cardStore) func(t *testing.T) { + return func(t *testing.T) { + item, err := card.FindCardByBuild(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.FindCard(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.FindCardData(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.FindCard(noContext, 3) + if err != nil { + t.Error(err) + return + } + err = store.DeleteCard(noContext, card.Id) + if err != nil { + t.Error(err) + return + } + _, err = store.FindCard(noContext, card.Step) + if got, want := sql.ErrNoRows, err; got != want { + t.Errorf("Want sql.ErrNoRows, got %v", got) + return + } + } +} diff --git a/store/card/scan.go b/store/card/scan.go new file mode 100644 index 00000000..9993eecd --- /dev/null +++ b/store/card/scan.go @@ -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.CreateCard) (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": 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 { + 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 +} diff --git a/store/shared/migrate/mysql/ddl_gen.go b/store/shared/migrate/mysql/ddl_gen.go index 05772ddb..05b70c23 100644 --- a/store/shared/migrate/mysql/ddl_gen.go +++ b/store/shared/migrate/mysql/ddl_gen.go @@ -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); +` diff --git a/store/shared/migrate/mysql/files/017_create_table_cards.sql b/store/shared/migrate/mysql/files/017_create_table_cards.sql new file mode 100644 index 00000000..c66c704f --- /dev/null +++ b/store/shared/migrate/mysql/files/017_create_table_cards.sql @@ -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 INDEX ix_cards_step ON cards (card_step); diff --git a/store/shared/migrate/postgres/ddl_gen.go b/store/shared/migrate/postgres/ddl_gen.go index cfcd1731..7671cf34 100644 --- a/store/shared/migrate/postgres/ddl_gen.go +++ b/store/shared/migrate/postgres/ddl_gen.go @@ -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); +` diff --git a/store/shared/migrate/postgres/files/018_create_table_cards.sql b/store/shared/migrate/postgres/files/018_create_table_cards.sql new file mode 100644 index 00000000..3447d748 --- /dev/null +++ b/store/shared/migrate/postgres/files/018_create_table_cards.sql @@ -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 INDEX IF NOT EXISTS ix_cards_step ON cards (card_step); diff --git a/store/shared/migrate/sqlite/ddl_gen.go b/store/shared/migrate/sqlite/ddl_gen.go index 55454a2f..f502eb3c 100644 --- a/store/shared/migrate/sqlite/ddl_gen.go +++ b/store/shared/migrate/sqlite/ddl_gen.go @@ -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); +` diff --git a/store/shared/migrate/sqlite/files/017_create_table_cards.sql b/store/shared/migrate/sqlite/files/017_create_table_cards.sql new file mode 100644 index 00000000..2e1fb9fc --- /dev/null +++ b/store/shared/migrate/sqlite/files/017_create_table_cards.sql @@ -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 INDEX IF NOT EXISTS ix_cards_step ON cards (card_step);