Tech QA Feedback & add support for organization level templates

This commit is contained in:
Eoin McAfee 2021-05-28 16:59:00 +01:00
parent a7ef135403
commit 88a5a8e796
34 changed files with 407 additions and 3009 deletions

View file

@ -87,6 +87,7 @@ tasks:
- cmd: go test -count=1 github.com/drone/drone/store/secret/global - cmd: go test -count=1 github.com/drone/drone/store/secret/global
- cmd: go test -count=1 github.com/drone/drone/store/stage - cmd: go test -count=1 github.com/drone/drone/store/stage
- cmd: go test -count=1 github.com/drone/drone/store/step - cmd: go test -count=1 github.com/drone/drone/store/step
- cmd: go test -count=1 github.com/drone/drone/store/template
- cmd: go test -count=1 github.com/drone/drone/store/user - cmd: go test -count=1 github.com/drone/drone/store/user
- cmd: docker kill mysql - cmd: docker kill mysql
@ -120,6 +121,7 @@ tasks:
- cmd: go test -count=1 github.com/drone/drone/store/secret/global - cmd: go test -count=1 github.com/drone/drone/store/secret/global
- cmd: go test -count=1 github.com/drone/drone/store/stage - cmd: go test -count=1 github.com/drone/drone/store/stage
- cmd: go test -count=1 github.com/drone/drone/store/step - cmd: go test -count=1 github.com/drone/drone/store/step
- cmd: go test -count=1 github.com/drone/drone/store/template
- cmd: go test -count=1 github.com/drone/drone/store/user - cmd: go test -count=1 github.com/drone/drone/store/user
- cmd: docker kill postgres - cmd: docker kill postgres
silent: true silent: true

View file

@ -34,21 +34,25 @@ type (
Template struct { Template struct {
Id int64 `json:"id,omitempty"` Id int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Data []byte `json:"data,omitempty"` Namespace string `json:"namespace,omitempty"`
Data string `json:"data,omitempty"`
Created int64 `json:"created,omitempty"` Created int64 `json:"created,omitempty"`
Updated int64 `json:"updated,omitempty"` Updated int64 `json:"updated,omitempty"`
} }
// TemplateStore manages repository templates. // TemplateStore manages repository templates.
TemplateStore interface { TemplateStore interface {
// List returns template list at org level
List(ctx context.Context, namespace string) ([]*Template, error)
// ListAll returns templates list from the datastore. // ListAll returns templates list from the datastore.
ListAll(ctx context.Context) ([]*Template, error) ListAll(ctx context.Context) ([]*Template, error)
// Find returns a template from the datastore. // Find returns a template from the datastore.
Find(ctx context.Context, id int64) (*Template, error) Find(ctx context.Context, id int64) (*Template, error)
// FindName returns a template from the datastore by name // FindName returns a template from the data store
FindName(ctx context.Context, name string) (*Template, error) FindName(ctx context.Context, name string, namespace string) (*Template, error)
// Create persists a new template to the datastore. // Create persists a new template to the datastore.
Create(ctx context.Context, template *Template) error Create(ctx context.Context, template *Template) error

View file

@ -15,10 +15,11 @@
package api package api
import ( import (
"github.com/drone/drone/handler/api/template"
"net/http" "net/http"
"os" "os"
"github.com/drone/drone/handler/api/template"
"github.com/drone/drone/core" "github.com/drone/drone/core"
"github.com/drone/drone/handler/api/acl" "github.com/drone/drone/handler/api/acl"
"github.com/drone/drone/handler/api/auth" "github.com/drone/drone/handler/api/auth"
@ -358,12 +359,13 @@ func (s Server) Handler() http.Handler {
}) })
r.Route("/templates", func(r chi.Router) { r.Route("/templates", func(r chi.Router) {
r.With(acl.CheckMembership(s.Orgs, false)).Get("/", template.HandleList(s.Template)) r.With(acl.CheckMembership(s.Orgs, false)).Get("/", template.HandleListAll(s.Template))
r.With(acl.CheckMembership(s.Orgs, true)).Post("/", template.HandleCreate(s.Template)) r.With(acl.CheckMembership(s.Orgs, true)).Post("/", template.HandleCreate(s.Template))
r.With(acl.CheckMembership(s.Orgs, false)).Get("/{name}", template.HandleFind(s.Template)) r.With(acl.CheckMembership(s.Orgs, false)).Get("/{namespace}", template.HandleList(s.Template))
r.With(acl.CheckMembership(s.Orgs, true)).Put("/{name}", template.HandleUpdate(s.Template)) r.With(acl.CheckMembership(s.Orgs, false)).Get("/{namespace}/{name}", template.HandleFind(s.Template))
r.With(acl.CheckMembership(s.Orgs, true)).Patch("/{name}", template.HandleUpdate(s.Template)) r.With(acl.CheckMembership(s.Orgs, true)).Put("/{namespace}/{name}", template.HandleUpdate(s.Template))
r.With(acl.CheckMembership(s.Orgs, true)).Delete("/{name}", template.HandleDelete(s.Template)) r.With(acl.CheckMembership(s.Orgs, true)).Patch("/{namespace}/{name}", template.HandleUpdate(s.Template))
r.With(acl.CheckMembership(s.Orgs, true)).Delete("/{namespace}/{name}", template.HandleDelete(s.Template))
}) })
r.Route("/system", func(r chi.Router) { r.Route("/system", func(r chi.Router) {

View file

@ -0,0 +1,26 @@
// 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 template
import (
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"net/http"
)
// HandleListAll returns an http.HandlerFunc that writes a json-encoded
// list of templates to the response body.
func HandleListAll(templateStore core.TemplateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
list, err := templateStore.ListAll(r.Context())
if err != nil {
render.NotFound(w, err)
return
}
render.JSON(w, list, 200)
}
}

View file

@ -0,0 +1,82 @@
// 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 template
import (
"context"
"encoding/json"
"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"
"net/http"
"net/http/httptest"
"testing"
)
var (
dummyTemplate = &core.Template{
Name: "my_template",
Data: "my_data",
Created: 1,
Updated: 2,
Namespace: "my_org",
}
dummyTemplateList = []*core.Template{
dummyTemplate,
}
)
func TestHandleAll(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().ListAll(gomock.Any()).Return(dummyTemplateList, nil)
c := new(chi.Context)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleListAll(templates).ServeHTTP(w, r)
if got, want := w.Code, http.StatusOK; want != got {
t.Errorf("Want response code %d, got %d", want, got)
}
}
func TestHandleAll_TemplateListErr(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().ListAll(gomock.Any()).Return(nil, errors.ErrNotFound)
c := new(chi.Context)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
r = r.WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, c),
)
HandleListAll(templates).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

@ -15,7 +15,8 @@ import (
type templateInput struct { type templateInput struct {
Name string `json:"name"` Name string `json:"name"`
Data []byte `json:"data"` Data string `json:"data"`
Namespace string `json:"namespace"`
} }
// HandleCreate returns an http.HandlerFunc that processes http // HandleCreate returns an http.HandlerFunc that processes http
@ -32,6 +33,7 @@ func HandleCreate(templateStore core.TemplateStore) http.HandlerFunc {
t := &core.Template{ t := &core.Template{
Name: in.Name, Name: in.Name,
Data: in.Data, Data: in.Data,
Namespace: in.Namespace,
} }
err = t.Validate() err = t.Validate()

View file

@ -51,7 +51,7 @@ func TestHandleCreate_ValidationErrorName(t *testing.T) {
c := new(chi.Context) c := new(chi.Context)
in := new(bytes.Buffer) in := new(bytes.Buffer)
json.NewEncoder(in).Encode(&core.Template{Name: "", Data: []byte("my_data")}) json.NewEncoder(in).Encode(&core.Template{Name: "", Data: "my_data"})
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in) r := httptest.NewRequest("GET", "/", in)
@ -77,7 +77,7 @@ func TestHandleCreate_ValidationErrorData(t *testing.T) {
c := new(chi.Context) c := new(chi.Context)
in := new(bytes.Buffer) in := new(bytes.Buffer)
json.NewEncoder(in).Encode(&core.Template{Name: "my_template", Data: nil}) json.NewEncoder(in).Encode(&core.Template{Name: "my_template", Data: ""})
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in) r := httptest.NewRequest("GET", "/", in)

View file

@ -19,8 +19,9 @@ func HandleDelete(template core.TemplateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var ( var (
name = chi.URLParam(r, "name") name = chi.URLParam(r, "name")
namespace = chi.URLParam(r, "namespace")
) )
s, err := template.FindName(r.Context(), name) s, err := template.FindName(r.Context(), name, namespace)
if err != nil { if err != nil {
render.NotFound(w, err) render.NotFound(w, err)
return return

View file

@ -24,11 +24,12 @@ func TestHandleDelete(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(dummyTemplate, nil) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(dummyTemplate, nil)
template.EXPECT().Delete(gomock.Any(), dummyTemplate).Return(nil) template.EXPECT().Delete(gomock.Any(), dummyTemplate).Return(nil)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
@ -47,10 +48,11 @@ func TestHandleDelete_TemplateNotFound(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(nil, errors.ErrNotFound) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(nil, errors.ErrNotFound)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
@ -75,11 +77,12 @@ func TestHandleDelete_DeleteError(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(dummyTemplate, nil) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(dummyTemplate, nil)
template.EXPECT().Delete(gomock.Any(), dummyTemplate).Return(errors.ErrNotFound) template.EXPECT().Delete(gomock.Any(), dummyTemplate).Return(errors.ErrNotFound)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)

View file

@ -19,8 +19,9 @@ func HandleFind(templateStore core.TemplateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var ( var (
name = chi.URLParam(r, "name") name = chi.URLParam(r, "name")
namespace = chi.URLParam(r, "namespace")
) )
template, err := templateStore.FindName(r.Context(), name) template, err := templateStore.FindName(r.Context(), name, namespace)
if err != nil { if err != nil {
render.NotFound(w, err) render.NotFound(w, err)
return return

View file

@ -24,10 +24,11 @@ func TestHandleFind(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(dummyTemplate, nil) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(dummyTemplate, nil)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
@ -46,10 +47,11 @@ func TestHandleFind_TemplateNotFound(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(nil, errors.ErrNotFound) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(nil, errors.ErrNotFound)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)

View file

@ -9,14 +9,16 @@ package template
import ( import (
"github.com/drone/drone/core" "github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render" "github.com/drone/drone/handler/api/render"
"github.com/go-chi/chi"
"net/http" "net/http"
) )
// HandleList returns an http.HandlerFunc that writes a json-encoded // HandleList returns an http.HandlerFunc that writes a json-encoded
// list of templates to the response body. // list of templates to the response body by namespace
func HandleList(templateStore core.TemplateStore) http.HandlerFunc { func HandleList(templateStore core.TemplateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
list, err := templateStore.ListAll(r.Context()) namespace := chi.URLParam(r, "namespace")
list, err := templateStore.List(r.Context(), namespace)
if err != nil { if err != nil {
render.NotFound(w, err) render.NotFound(w, err)
return return

View file

@ -1,15 +1,12 @@
package template
// Copyright 2019 Drone.IO Inc. All rights reserved. // Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License // Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file. // that can be found in the LICENSE file.
// +build !oss
package template
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/errors" "github.com/drone/drone/handler/api/errors"
"github.com/drone/drone/mock" "github.com/drone/drone/mock"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -20,26 +17,15 @@ import (
"testing" "testing"
) )
var (
dummyTemplate = &core.Template{
Name: "my_template",
Data: []byte("my_data"),
Created: 1,
Updated: 2,
}
dummyTemplateList = []*core.Template{
dummyTemplate,
}
)
func TestHandleList(t *testing.T) { func TestHandleList(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
templates := mock.NewMockTemplateStore(controller) templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().ListAll(gomock.Any()).Return(dummyTemplateList, nil) templates.EXPECT().List(gomock.Any(), dummyTemplate.Namespace).Return(dummyTemplateList, nil)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
@ -58,9 +44,10 @@ func TestHandleList_TemplateListErr(t *testing.T) {
defer controller.Finish() defer controller.Finish()
templates := mock.NewMockTemplateStore(controller) templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().ListAll(gomock.Any()).Return(nil, errors.ErrNotFound) templates.EXPECT().List(gomock.Any(), dummyTemplate.Namespace).Return(nil, errors.ErrNotFound)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("namespace", "my_org")
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)

View file

@ -13,8 +13,8 @@ import (
) )
type templateUpdate struct { type templateUpdate struct {
Data *[]byte `json:"data"` Data *string `json:"data"`
Updated *int64 `json:"Updated"` Namespace *string `json:"namespace"`
} }
// HandleUpdate returns an http.HandlerFunc that processes http // HandleUpdate returns an http.HandlerFunc that processes http
@ -23,6 +23,7 @@ func HandleUpdate(templateStore core.TemplateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var ( var (
name = chi.URLParam(r, "name") name = chi.URLParam(r, "name")
namespace = chi.URLParam(r, "namespace")
) )
in := new(templateUpdate) in := new(templateUpdate)
@ -32,7 +33,7 @@ func HandleUpdate(templateStore core.TemplateStore) http.HandlerFunc {
return return
} }
s, err := templateStore.FindName(r.Context(), name) s, err := templateStore.FindName(r.Context(), name, namespace)
if err != nil { if err != nil {
render.NotFound(w, err) render.NotFound(w, err)
return return
@ -41,8 +42,8 @@ func HandleUpdate(templateStore core.TemplateStore) http.HandlerFunc {
if in.Data != nil { if in.Data != nil {
s.Data = *in.Data s.Data = *in.Data
} }
if in.Updated != nil { if in.Namespace != nil {
s.Updated = *in.Updated s.Namespace = *in.Namespace
} }
err = s.Validate() err = s.Validate()

View file

@ -26,11 +26,12 @@ func TestHandleUpdate(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(dummyTemplate, nil) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(dummyTemplate, nil)
template.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) template.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
in := new(bytes.Buffer) in := new(bytes.Buffer)
json.NewEncoder(in).Encode(dummyTemplate) json.NewEncoder(in).Encode(dummyTemplate)
@ -52,10 +53,11 @@ func TestHandleUpdate_ValidationErrorData(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(&core.Template{Name: "my_template"}, nil) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(&core.Template{Name: "my_template"}, nil)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
in := new(bytes.Buffer) in := new(bytes.Buffer)
json.NewEncoder(in).Encode(&core.Secret{Data: ""}) json.NewEncoder(in).Encode(&core.Secret{Data: ""})
@ -83,10 +85,11 @@ func TestHandleUpdate_TemplateNotFound(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(nil, errors.ErrNotFound) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(nil, errors.ErrNotFound)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
in := new(bytes.Buffer) in := new(bytes.Buffer)
json.NewEncoder(in).Encode(&core.Secret{}) json.NewEncoder(in).Encode(&core.Secret{})
@ -114,14 +117,15 @@ func TestHandleUpdate_UpdateError(t *testing.T) {
defer controller.Finish() defer controller.Finish()
template := mock.NewMockTemplateStore(controller) template := mock.NewMockTemplateStore(controller)
template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name).Return(&core.Template{Name: "my_template"}, nil) template.EXPECT().FindName(gomock.Any(), dummyTemplate.Name, dummyTemplate.Namespace).Return(&core.Template{Name: "my_template"}, nil)
template.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.ErrNotFound) template.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.ErrNotFound)
c := new(chi.Context) c := new(chi.Context)
c.URLParams.Add("name", "my_template") c.URLParams.Add("name", "my_template")
c.URLParams.Add("namespace", "my_org")
in := new(bytes.Buffer) in := new(bytes.Buffer)
json.NewEncoder(in).Encode(&core.Template{Data: []byte("my_data")}) json.NewEncoder(in).Encode(&core.Template{Data: "my_data"})
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", in) r := httptest.NewRequest("GET", "/", in)

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@ package converter
import ( import (
"context" "context"
"github.com/drone/drone/core" "github.com/drone/drone/core"
"github.com/drone/drone/plugin/converter/parser" "github.com/drone/drone/plugin/converter/starlark"
"strings" "strings"
) )
@ -48,7 +48,7 @@ func (p *starlarkPlugin) Convert(ctx context.Context, req *core.ConvertArgs) (*c
return nil, nil return nil, nil
} }
file, _ := parser.ParseStarlark(req, nil, nil) file, _ := starlark.Parse(req, nil, nil)
return &core.Config{ return &core.Config{
Data: *file, Data: *file,
}, nil }, nil

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package parser package starlark
import ( import (
"github.com/drone/drone/core" "github.com/drone/drone/core"

View file

@ -1,4 +1,18 @@
package parser // 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 starlark
import ( import (
"bytes" "bytes"
@ -38,7 +52,7 @@ var (
ErrCannotLoad = errors.New("starlark: cannot load external scripts") ErrCannotLoad = errors.New("starlark: cannot load external scripts")
) )
func ParseStarlark(req *core.ConvertArgs, template *core.Template, templateData map[string]interface{}) (file *string, err error) { func Parse(req *core.ConvertArgs, template *core.Template, templateData map[string]interface{}) (file *string, err error) {
thread := &starlark.Thread{ thread := &starlark.Thread{
Name: "drone", Name: "drone",
Load: noLoad, Load: noLoad,
@ -49,13 +63,13 @@ func ParseStarlark(req *core.ConvertArgs, template *core.Template, templateData
}).Traceln(msg) }).Traceln(msg)
}, },
} }
var starlarkFile []byte var starlarkFile string
var starlarkFileName string var starlarkFileName string
if template != nil { if template != nil {
starlarkFile = template.Data starlarkFile = template.Data
starlarkFileName = template.Name starlarkFileName = template.Name
} else { } else {
starlarkFile = []byte(req.Config.Data) starlarkFile = req.Config.Data
starlarkFileName = req.Repo.Config starlarkFileName = req.Repo.Config
} }

View file

@ -1,4 +1,18 @@
package parser // 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 starlark
import ( import (
"github.com/drone/drone/core" "github.com/drone/drone/core"
@ -31,7 +45,7 @@ func TestParseStarlark(t *testing.T) {
} }
template := &core.Template{ template := &core.Template{
Name: "my_template.star", Name: "my_template.star",
Data: before, Data: string(before),
} }
templateData := map[string]interface{}{ templateData := map[string]interface{}{
@ -42,7 +56,7 @@ func TestParseStarlark(t *testing.T) {
req.Config.Data = string(before) req.Config.Data = string(before)
parsedFile, err := ParseStarlark(req, template, templateData) parsedFile, err := Parse(req, template, templateData)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -80,7 +94,7 @@ func TestParseStarlarkNotTemplateFile(t *testing.T) {
req.Repo.Config = "plugin.starlark.star" req.Repo.Config = "plugin.starlark.star"
req.Config.Data = string(before) req.Config.Data = string(before)
parsedFile, err := ParseStarlark(req, nil, nil) parsedFile, err := Parse(req, nil, nil)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package parser package starlark
import ( import (
"encoding/json" "encoding/json"

View file

@ -1,15 +1,31 @@
// 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 converter package converter
import ( import (
"context" "context"
"errors" "errors"
"github.com/drone/drone/core" "github.com/drone/drone/core"
"github.com/drone/drone/plugin/converter/parser" "github.com/drone/drone/plugin/converter/starlark"
"gopkg.in/yaml.v2"
"regexp" "regexp"
"strings" "strings"
"gopkg.in/yaml.v2"
) )
var ( var (
@ -45,7 +61,7 @@ func (p *templatePlugin) Convert(ctx context.Context, req *core.ConvertArgs) (*c
return nil, ErrTemplateSyntaxErrors return nil, ErrTemplateSyntaxErrors
} }
// get template from db // get template from db
template, err := p.templateStore.FindName(ctx, templateArgs.Load) template, err := p.templateStore.FindName(ctx, templateArgs.Load, req.Repo.Namespace)
if err != nil { if err != nil {
return nil, nil return nil, nil
} }
@ -57,7 +73,7 @@ func (p *templatePlugin) Convert(ctx context.Context, req *core.ConvertArgs) (*c
strings.HasSuffix(templateArgs.Load, ".star") || strings.HasSuffix(templateArgs.Load, ".star") ||
strings.HasSuffix(templateArgs.Load, ".starlark") { strings.HasSuffix(templateArgs.Load, ".starlark") {
file, err := parser.ParseStarlark(req, template, templateArgs.Data) file, err := starlark.Parse(req, template, templateArgs.Data)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,3 +1,17 @@
// 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 converter package converter
import ( import (
@ -9,6 +23,26 @@ import (
) )
func TestTemplatePluginConvert(t *testing.T) { func TestTemplatePluginConvert(t *testing.T) {
templateArgs, err := ioutil.ReadFile("testdata/starlark.template.yml")
if err != nil {
t.Error(err)
return
}
req := &core.ConvertArgs{
Build: &core.Build{
After: "3d21ec53a331a6f037a91c368710b99387d012c1",
},
Repo: &core.Repository{
Slug: "octocat/hello-world",
Config: ".drone.yml",
Namespace: "octocat",
},
Config: &core.Config{
Data: string(templateArgs),
},
}
beforeInput, err := ioutil.ReadFile("testdata/starlark.input.star") beforeInput, err := ioutil.ReadFile("testdata/starlark.input.star")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -21,37 +55,18 @@ func TestTemplatePluginConvert(t *testing.T) {
return return
} }
templateArgs, err := ioutil.ReadFile("testdata/starlark.template.yml")
if err != nil {
t.Error(err)
return
}
template := &core.Template{ template := &core.Template{
Name: "plugin.starlark", Name: "plugin.starlark",
Data: beforeInput, Data: string(beforeInput),
} }
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
templates := mock.NewMockTemplateStore(controller) templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().FindName(gomock.Any(), template.Name).Return(template, nil) templates.EXPECT().FindName(gomock.Any(), template.Name, req.Repo.Namespace).Return(template, nil)
plugin := Template(templates) plugin := Template(templates)
req := &core.ConvertArgs{
Build: &core.Build{
After: "3d21ec53a331a6f037a91c368710b99387d012c1",
},
Repo: &core.Repository{
Slug: "octocat/hello-world",
Config: ".drone.yml",
},
Config: &core.Config{
Data: string(templateArgs),
},
}
config, err := plugin.Convert(noContext, req) config, err := plugin.Convert(noContext, req)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -121,24 +136,12 @@ func TestTemplatePluginConvertDroneFileTypePipeline(t *testing.T) {
} }
func TestTemplatePluginConvertTemplateNotFound(t *testing.T) { func TestTemplatePluginConvertTemplateNotFound(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
template := &core.Template{
Name: "plugin.starlark",
Data: nil,
}
templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().FindName(gomock.Any(), template.Name).Return(nil, nil)
templateArgs, err := ioutil.ReadFile("testdata/starlark.template.yml") templateArgs, err := ioutil.ReadFile("testdata/starlark.template.yml")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
plugin := Template(templates)
req := &core.ConvertArgs{ req := &core.ConvertArgs{
Build: &core.Build{ Build: &core.Build{
After: "3d21ec53a331a6f037a91c368710b99387d012c1", After: "3d21ec53a331a6f037a91c368710b99387d012c1",
@ -146,10 +149,24 @@ func TestTemplatePluginConvertTemplateNotFound(t *testing.T) {
Repo: &core.Repository{ Repo: &core.Repository{
Slug: "octocat/hello-world", Slug: "octocat/hello-world",
Config: ".drone.yml", Config: ".drone.yml",
Namespace: "octocat",
}, },
Config: &core.Config{Data: string(templateArgs)}, Config: &core.Config{Data: string(templateArgs)},
} }
controller := gomock.NewController(t)
defer controller.Finish()
template := &core.Template{
Name: "plugin.starlark",
Data: "",
}
templates := mock.NewMockTemplateStore(controller)
templates.EXPECT().FindName(gomock.Any(), template.Name, req.Repo.Namespace).Return(nil, nil)
plugin := Template(templates)
config, err := plugin.Convert(noContext, req) config, err := plugin.Convert(noContext, req)
if config != nil { if config != nil {
t.Errorf("template converter: template name given not found") t.Errorf("template converter: template name given not found")

18
plugin/converter/testdata/input.jsonnet vendored Normal file
View file

@ -0,0 +1,18 @@
local stepName = std.extVar("input.my_step");
local image = std.extVar("input.my_image");
local commands = std.extVar("input.my_command");
{
"kind": "pipeline",
"type": "docker",
"name": "default",
"steps": [
{
"name": stepName,
"image": image,
"commands": [
commands
]
}
]
}

View file

@ -0,0 +1,6 @@
kind: template
load: plugin.jsonnet
data:
stepName: my_step
image: my_image
commands: my_command

View file

@ -664,13 +664,14 @@ CREATE INDEX ix_latest_repo ON latest (latest_repo_id);
` `
// //
// 015_create_table_template.sql // 015_create_table_templates.sql
// //
var createTableTemplate = ` var createTableTemplate = `
CREATE TABLE IF NOT EXISTS template ( CREATE TABLE IF NOT EXISTS templates (
template_id INTEGER PRIMARY KEY AUTO_INCREMENT template_id INTEGER PRIMARY KEY AUTO_INCREMENT
,template_name VARCHAR(500) ,template_name VARCHAR(500)
,template_namespace VARCHAR(50)
,template_data BLOB ,template_data BLOB
,template_created INTEGER ,template_created INTEGER
,template_updated INTEGER ,template_updated INTEGER

View file

@ -1,8 +1,9 @@
-- name: create-table-template -- name: create-table-template
CREATE TABLE IF NOT EXISTS template ( CREATE TABLE IF NOT EXISTS templates (
template_id INTEGER PRIMARY KEY AUTO_INCREMENT template_id INTEGER PRIMARY KEY AUTO_INCREMENT
,template_name VARCHAR(500) ,template_name VARCHAR(500)
,template_namespace VARCHAR(50)
,template_data BLOB ,template_data BLOB
,template_created INTEGER ,template_created INTEGER
,template_updated INTEGER ,template_updated INTEGER

View file

@ -642,13 +642,14 @@ CREATE INDEX IF NOT EXISTS ix_latest_repo ON latest (latest_repo_id);
` `
// //
// 016_create_template_table.sql // 016_create_template_tables.sql
// //
var createTableTemplate = ` var createTableTemplate = `
CREATE TABLE IF NOT EXISTS template ( CREATE TABLE IF NOT EXISTS templates (
template_id SERIAL PRIMARY KEY template_id SERIAL PRIMARY KEY
,template_name TEXT UNIQUE ,template_name TEXT UNIQUE
,template_namespace VARCHAR(50)
,template_data BYTEA ,template_data BYTEA
,template_created INTEGER ,template_created INTEGER
,template_updated INTEGER ,template_updated INTEGER

View file

@ -1,8 +1,9 @@
-- name: create-table-template -- name: create-table-template
CREATE TABLE IF NOT EXISTS template ( CREATE TABLE IF NOT EXISTS templates (
template_id SERIAL PRIMARY KEY template_id SERIAL PRIMARY KEY
,template_name TEXT UNIQUE ,template_name TEXT UNIQUE
,template_namespace VARCHAR(50)
,template_data BYTEA ,template_data BYTEA
,template_created INTEGER ,template_created INTEGER
,template_updated INTEGER ,template_updated INTEGER

View file

@ -644,13 +644,14 @@ CREATE INDEX IF NOT EXISTS ix_latest_repo ON latest (latest_repo_id);
` `
// //
// 015_create_template_table.sql // 015_create_template_tables.sql
// //
var createTableTemplate = ` var createTableTemplate = `
CREATE TABLE IF NOT EXISTS template ( CREATE TABLE IF NOT EXISTS templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT template_id INTEGER PRIMARY KEY AUTOINCREMENT
,template_name TEXT UNIQUE ,template_name TEXT UNIQUE
,template_namespace TEXT COLLATE NOCASE
,template_data BLOB ,template_data BLOB
,template_created INTEGER ,template_created INTEGER
,template_updated INTEGER ,template_updated INTEGER

View file

@ -1,8 +1,9 @@
-- name: create-table-template -- name: create-table-template
CREATE TABLE IF NOT EXISTS template ( CREATE TABLE IF NOT EXISTS templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT template_id INTEGER PRIMARY KEY AUTOINCREMENT
,template_name TEXT UNIQUE ,template_name TEXT UNIQUE
,template_namespace TEXT COLLATE NOCASE
,template_data BLOB ,template_data BLOB
,template_created INTEGER ,template_created INTEGER
,template_updated INTEGER ,template_updated INTEGER

View file

@ -8,6 +8,7 @@ package template
import ( import (
"database/sql" "database/sql"
"github.com/drone/drone/core" "github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db" "github.com/drone/drone/store/shared/db"
) )
@ -18,6 +19,7 @@ func toParams(template *core.Template) (map[string]interface{}, error) {
return map[string]interface{}{ return map[string]interface{}{
"template_id": template.Id, "template_id": template.Id,
"template_name": template.Name, "template_name": template.Name,
"template_namespace": template.Namespace,
"template_data": template.Data, "template_data": template.Data,
"template_created": template.Created, "template_created": template.Created,
"template_updated": template.Updated, "template_updated": template.Updated,
@ -30,6 +32,7 @@ func scanRow(scanner db.Scanner, dst *core.Template) error {
err := scanner.Scan( err := scanner.Scan(
&dst.Id, &dst.Id,
&dst.Name, &dst.Name,
&dst.Namespace,
&dst.Data, &dst.Data,
&dst.Created, &dst.Created,
&dst.Updated, &dst.Updated,

View file

@ -23,6 +23,24 @@ type templateStore struct {
db *db.DB db *db.DB
} }
func (s *templateStore) List(ctx context.Context, namespace string) ([]*core.Template, error) {
var out []*core.Template
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
params := map[string]interface{}{"template_namespace": namespace}
stmt, args, err := binder.BindNamed(queryNamespace, 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 (s *templateStore) ListAll(ctx context.Context) ([]*core.Template, error) { func (s *templateStore) ListAll(ctx context.Context) ([]*core.Template, error) {
var out []*core.Template var out []*core.Template
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error { err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
@ -58,14 +76,16 @@ func (s *templateStore) Find(ctx context.Context, id int64) (*core.Template, err
return out, err return out, err
} }
func (s *templateStore) FindName(ctx context.Context, name string) (*core.Template, error) { func (s *templateStore) FindName(ctx context.Context, name string, namespace string) (*core.Template, error) {
out := &core.Template{Name: name} out := &core.Template{Name: name, Namespace: namespace}
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error { err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
params, err := toParams(out) params, err := toParams(out)
if err != nil { if err != nil {
return err return err
} }
query, args, err := binder.BindNamed(queryName, params) query, args, err := binder.BindNamed(queryName, params)
if err != nil { if err != nil {
return err return err
} }
@ -146,7 +166,7 @@ func (s *templateStore) Delete(ctx context.Context, template *core.Template) err
} }
const queryKey = queryBase + ` const queryKey = queryBase + `
FROM template FROM templates
WHERE template_id = :template_id WHERE template_id = :template_id
LIMIT 1 LIMIT 1
` `
@ -155,24 +175,33 @@ const queryBase = `
SELECT SELECT
template_id template_id
,template_name ,template_name
,template_namespace
,template_data ,template_data
,template_created ,template_created
,template_updated ,template_updated
` `
const queryAll = queryBase + ` const queryAll = queryBase + `
FROM template FROM templates
ORDER BY template_name
`
const queryNamespace = queryBase + `
FROM templates
WHERE template_namespace = :template_namespace
ORDER BY template_name ORDER BY template_name
` `
const stmtInsert = ` const stmtInsert = `
INSERT INTO template ( INSERT INTO templates (
template_name template_name
,template_namespace
,template_data ,template_data
,template_created ,template_created
,template_updated ,template_updated
) VALUES ( ) VALUES (
:template_name :template_name
,:template_namespace
,:template_data ,:template_data
,:template_created ,:template_created
,:template_updated ,:template_updated
@ -180,20 +209,22 @@ INSERT INTO template (
` `
const stmtUpdate = ` const stmtUpdate = `
UPDATE template SET UPDATE templates SET
template_name = :template_name template_name = :template_name
,template_namespace = :template_namespace
,template_data = :template_data ,template_data = :template_data
,template_updated = :template_updated ,template_updated = :template_updated
WHERE template_id = :template_id WHERE template_id = :template_id
` `
const stmtDelete = ` const stmtDelete = `
DELETE FROM template DELETE FROM templates
WHERE template_id = :template_id WHERE template_id = :template_id
` `
const queryName = queryBase + ` const queryName = queryBase + `
FROM template FROM templates
WHERE template_name = :template_name WHERE template_name = :template_name
AND template_namespace = :template_namespace
LIMIT 1 LIMIT 1
` `

View file

@ -7,7 +7,6 @@
package template package template
import ( import (
"bytes"
"context" "context"
"database/sql" "database/sql"
"github.com/drone/drone/core" "github.com/drone/drone/core"
@ -37,7 +36,8 @@ func testTemplateCreate(store *templateStore) func(t *testing.T) {
item := &core.Template{ item := &core.Template{
Id: 1, Id: 1,
Name: "my_template", Name: "my_template",
Data: []byte("some_template_data"), Namespace: "my_org",
Data: "some_template_data",
Created: 1, Created: 1,
Updated: 2, Updated: 2,
} }
@ -52,6 +52,7 @@ func testTemplateCreate(store *templateStore) func(t *testing.T) {
t.Run("Find", testTemplateFind(store, item)) t.Run("Find", testTemplateFind(store, item))
t.Run("FindName", testTemplateFindName(store)) t.Run("FindName", testTemplateFindName(store))
t.Run("ListAll", testTemplateListAll(store)) t.Run("ListAll", testTemplateListAll(store))
t.Run("List", testTemplateList(store))
t.Run("Update", testTemplateUpdate(store)) t.Run("Update", testTemplateUpdate(store))
t.Run("Delete", testTemplateDelete(store)) t.Run("Delete", testTemplateDelete(store))
} }
@ -70,7 +71,7 @@ func testTemplateFind(store *templateStore, template *core.Template) func(t *tes
func testTemplateFindName(store *templateStore) func(t *testing.T) { func testTemplateFindName(store *templateStore) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
item, err := store.FindName(noContext, "my_template") item, err := store.FindName(noContext, "my_template", "my_org")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} else { } else {
@ -84,9 +85,12 @@ func testTemplate(item *core.Template) func(t *testing.T) {
if got, want := item.Name, "my_template"; got != want { if got, want := item.Name, "my_template"; got != want {
t.Errorf("Want template name %q, got %q", want, got) t.Errorf("Want template name %q, got %q", want, got)
} }
if got, want := item.Data, []byte("some_template_data"); bytes.Compare(got, want) != 0 { if got, want := item.Data, "some_template_data"; got != want {
t.Errorf("Want template data %q, got %q", want, got) t.Errorf("Want template data %q, got %q", want, got)
} }
if got, want := item.Namespace, "my_org"; got != want {
t.Errorf("Want template org %q, got %q", want, got)
}
} }
} }
@ -105,9 +109,24 @@ func testTemplateListAll(store *templateStore) func(t *testing.T) {
} }
} }
func testTemplateList(store *templateStore) func(t *testing.T) {
return func(t *testing.T) {
list, err := store.List(noContext, "my_org")
if err != nil {
t.Error(err)
return
}
if got, want := len(list), 1; got != want {
t.Errorf("Want count %d, got %d", want, got)
} else {
t.Run("Fields", testTemplate(list[0]))
}
}
}
func testTemplateUpdate(store *templateStore) func(t *testing.T) { func testTemplateUpdate(store *templateStore) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
before, err := store.FindName(noContext, "my_template") before, err := store.FindName(noContext, "my_template", "my_org")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -130,7 +149,7 @@ func testTemplateUpdate(store *templateStore) func(t *testing.T) {
func testTemplateDelete(store *templateStore) func(t *testing.T) { func testTemplateDelete(store *templateStore) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
secret, err := store.FindName(noContext, "my_template") secret, err := store.FindName(noContext, "my_template", "my_org")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return