Revert "Add new registry type: LocalHarbor"

This reverts commit 94cacf762a.

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2019-04-17 13:03:11 +08:00
parent c754ec054d
commit 3f7884d9d2
12 changed files with 87 additions and 202 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/goharbor/harbor/src/core/api/models" "github.com/goharbor/harbor/src/core/api/models"
"github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/adapter" "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
"github.com/goharbor/harbor/src/replication/policy" "github.com/goharbor/harbor/src/replication/policy"
"github.com/goharbor/harbor/src/replication/registry" "github.com/goharbor/harbor/src/replication/registry"
@ -185,25 +186,12 @@ func (t *RegistryAPI) Post() {
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return return
} }
if reg != nil { if reg != nil {
t.HandleConflict(fmt.Sprintf("name '%s' is already used", r.Name)) t.HandleConflict(fmt.Sprintf("name '%s' is already used", r.Name))
return return
} }
if r.Type == model.RegistryTypeLocalHarbor {
n, _, err := t.manager.List(&model.RegistryQuery{
Type: string(model.RegistryTypeLocalHarbor),
})
if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to list registries: %v", err))
return
}
if n > 0 {
t.HandleBadRequest(fmt.Sprintf("can only add one registry whose type is %s", model.RegistryTypeLocalHarbor))
return
}
}
status, err := registry.CheckHealthStatus(r) status, err := registry.CheckHealthStatus(r)
if err != nil { if err != nil {
t.HandleBadRequest(fmt.Sprintf("health check to registry %s failed: %v", r.URL, err)) t.HandleBadRequest(fmt.Sprintf("health check to registry %s failed: %v", r.URL, err))
@ -362,19 +350,26 @@ func (t *RegistryAPI) Delete() {
// GetInfo returns the base info and capability declarations of the registry // GetInfo returns the base info and capability declarations of the registry
func (t *RegistryAPI) GetInfo() { func (t *RegistryAPI) GetInfo() {
id, err := t.GetInt64FromPath(":id") id, err := t.GetInt64FromPath(":id")
if err != nil || id <= 0 { // "0" is used for the ID of the local Harbor registry
if err != nil || id < 0 {
t.HandleBadRequest(fmt.Sprintf("invalid registry ID %s", t.GetString(":id"))) t.HandleBadRequest(fmt.Sprintf("invalid registry ID %s", t.GetString(":id")))
return return
} }
registry, err := t.manager.Get(id) var registry *model.Registry
if err != nil { if id == 0 {
t.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", id, err)) registry = event.GetLocalRegistry()
return } else {
} registry, err = t.manager.Get(id)
if registry == nil { if err != nil {
t.HandleNotFound(fmt.Sprintf("registry %d not found", id)) t.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", id, err))
return return
}
if registry == nil {
t.HandleNotFound(fmt.Sprintf("registry %d not found", id))
return
}
} }
factory, err := adapter.GetFactory(registry.Type) factory, err := adapter.GetFactory(registry.Type)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to get the adapter factory for registry type %s: %v", registry.Type, err)) t.HandleInternalServerError(fmt.Sprintf("failed to get the adapter factory for registry type %s: %v", registry.Type, err))

View File

@ -89,9 +89,6 @@ func (f *fakedPolicyManager) Get(id int64) (*model.Policy, error) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, nil }, nil
} }
if id == 2 { if id == 2 {
@ -202,7 +199,7 @@ func TestCreateExecution(t *testing.T) {
}, },
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
// 400 the policy is disabled // 400
{ {
request: &testingRequest{ request: &testingRequest{
method: http.MethodPost, method: http.MethodPost,

View File

@ -26,6 +26,8 @@ import (
"github.com/goharbor/harbor/src/replication/registry" "github.com/goharbor/harbor/src/replication/registry"
) )
// TODO rename the file to "replication.go"
// ReplicationPolicyAPI handles the replication policy requests // ReplicationPolicyAPI handles the replication policy requests
type ReplicationPolicyAPI struct { type ReplicationPolicyAPI struct {
BaseController BaseController
@ -103,30 +105,21 @@ func (r *ReplicationPolicyAPI) validateName(policy *model.Policy) bool {
// make sure the registry referred exists // make sure the registry referred exists
func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool { func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool {
srcRegistry, err := replication.RegistryMgr.Get(policy.SrcRegistry.ID) var registryID int64
if policy.SrcRegistry != nil && policy.SrcRegistry.ID > 0 {
registryID = policy.SrcRegistry.ID
} else {
registryID = policy.DestRegistry.ID
}
registry, err := replication.RegistryMgr.Get(registryID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get source registry %d: %v", policy.SrcRegistry.ID, err)) r.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", registryID, err))
return false return false
} }
if srcRegistry == nil { if registry == nil {
r.HandleNotFound(fmt.Sprintf("source registry %d not found", policy.SrcRegistry.ID)) r.HandleNotFound(fmt.Sprintf("registry %d not found", registryID))
return false return false
} }
dstRegistry, err := replication.RegistryMgr.Get(policy.DestRegistry.ID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get destination registry %d: %v", policy.DestRegistry.ID, err))
return false
}
if dstRegistry == nil {
r.HandleNotFound(fmt.Sprintf("destination registry %d not found", policy.DestRegistry.ID))
return false
}
// one of the source registry or destination registry must be local Harbor
if srcRegistry.Type != model.RegistryTypeLocalHarbor && dstRegistry.Type != model.RegistryTypeLocalHarbor {
r.HandleBadRequest(fmt.Sprintf("at least one of the registries' type is %s", model.RegistryTypeLocalHarbor))
return false
}
return true return true
} }

View File

@ -22,6 +22,8 @@ import (
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
) )
// TODO rename the file to "replication.go"
type fakedRegistryManager struct{} type fakedRegistryManager struct{}
func (f *fakedRegistryManager) Add(*model.Registry) (int64, error) { func (f *fakedRegistryManager) Add(*model.Registry) (int64, error) {
@ -33,12 +35,7 @@ func (f *fakedRegistryManager) List(...*model.RegistryQuery) (int64, []*model.Re
func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) { func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) {
if id == 1 { if id == 1 {
return &model.Registry{ return &model.Registry{
Type: model.RegistryTypeLocalHarbor, Type: "faked_registry",
}, nil
}
if id == 2 {
return &model.Registry{
Type: model.RegistryTypeHarbor,
}, nil }, nil
} }
return nil, nil return nil, nil
@ -131,14 +128,11 @@ func TestReplicationPolicyAPICreate(t *testing.T) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, },
}, },
code: http.StatusBadRequest, code: http.StatusBadRequest,
}, },
// 400 empty source registry // 400 empty registry
{ {
request: &testingRequest{ request: &testingRequest{
method: http.MethodPost, method: http.MethodPost,
@ -146,29 +140,10 @@ func TestReplicationPolicyAPICreate(t *testing.T) {
credential: sysAdmin, credential: sysAdmin,
bodyJSON: &model.Policy{ bodyJSON: &model.Policy{
Name: "policy01", Name: "policy01",
DestRegistry: &model.Registry{
ID: 1,
},
}, },
}, },
code: http.StatusBadRequest, code: http.StatusBadRequest,
}, },
// 400 empty destination registry
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/replication/policies",
credential: sysAdmin,
bodyJSON: &model.Policy{
Name: "policy01",
SrcRegistry: &model.Registry{
ID: 1,
},
},
},
code: http.StatusBadRequest,
},
// 409, duplicate policy name // 409, duplicate policy name
{ {
request: &testingRequest{ request: &testingRequest{
@ -180,9 +155,6 @@ func TestReplicationPolicyAPICreate(t *testing.T) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, },
}, },
code: http.StatusConflict, code: http.StatusConflict,
@ -196,33 +168,12 @@ func TestReplicationPolicyAPICreate(t *testing.T) {
bodyJSON: &model.Policy{ bodyJSON: &model.Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 2,
},
DestRegistry: &model.Registry{
ID: 3,
}, },
}, },
}, },
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
// 400 both registry types are not local harbor
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/replication/policies",
credential: sysAdmin,
bodyJSON: &model.Policy{
Name: "policy01",
SrcRegistry: &model.Registry{
ID: 2,
},
DestRegistry: &model.Registry{
ID: 2,
},
},
},
code: http.StatusBadRequest,
},
// 201 // 201
{ {
request: &testingRequest{ request: &testingRequest{
@ -234,9 +185,6 @@ func TestReplicationPolicyAPICreate(t *testing.T) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, },
}, },
code: http.StatusCreated, code: http.StatusCreated,
@ -343,9 +291,6 @@ func TestReplicationPolicyAPIUpdate(t *testing.T) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, },
}, },
code: http.StatusBadRequest, code: http.StatusBadRequest,
@ -361,9 +306,6 @@ func TestReplicationPolicyAPIUpdate(t *testing.T) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, },
}, },
code: http.StatusConflict, code: http.StatusConflict,
@ -377,33 +319,12 @@ func TestReplicationPolicyAPIUpdate(t *testing.T) {
bodyJSON: &model.Policy{ bodyJSON: &model.Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 3,
},
DestRegistry: &model.Registry{
ID: 2, ID: 2,
}, },
}, },
}, },
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
// 400 both registry types are not local harbor
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/replication/policies/1",
credential: sysAdmin,
bodyJSON: &model.Policy{
Name: "policy01",
SrcRegistry: &model.Registry{
ID: 2,
},
DestRegistry: &model.Registry{
ID: 2,
},
},
},
code: http.StatusBadRequest,
},
// 200 // 200
{ {
request: &testingRequest{ request: &testingRequest{
@ -415,9 +336,6 @@ func TestReplicationPolicyAPIUpdate(t *testing.T) {
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &model.Registry{
ID: 2,
},
}, },
}, },
code: http.StatusOK, code: http.StatusOK,

View File

@ -37,14 +37,6 @@ func init() {
return return
} }
log.Infof("the factory for adapter %s registered", model.RegistryTypeHarbor) log.Infof("the factory for adapter %s registered", model.RegistryTypeHarbor)
if err := adp.RegisterFactory(model.RegistryTypeLocalHarbor, func(registry *model.Registry) (adp.Adapter, error) {
return newAdapter(registry)
}); err != nil {
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeLocalHarbor, err)
return
}
log.Infof("the factory for adapter %s registered", model.RegistryTypeLocalHarbor)
} }
type adapter struct { type adapter struct {

View File

@ -10,7 +10,6 @@ import (
type ListRegistryQuery struct { type ListRegistryQuery struct {
// Query is name query // Query is name query
Query string Query string
Type string
// Offset specifies the offset in the registry list to return // Offset specifies the offset in the registry list to return
Offset int64 Offset int64
// Limit specifies the maximum registries to return // Limit specifies the maximum registries to return
@ -62,13 +61,8 @@ func ListRegistries(query ...*ListRegistryQuery) (int64, []*models.Registry, err
o := dao.GetOrmer() o := dao.GetOrmer()
q := o.QueryTable(&models.Registry{}) q := o.QueryTable(&models.Registry{})
if len(query) > 0 && query[0] != nil { if len(query) > 0 && len(query[0].Query) > 0 {
if len(query[0].Query) > 0 { q = q.Filter("name__contains", query[0].Query)
q = q.Filter("name__contains", query[0].Query)
}
if len(query[0].Type) > 0 {
q = q.Filter("type", query[0].Type)
}
} }
total, err := q.Count() total, err := q.Count()

View File

@ -21,6 +21,7 @@ import (
"github.com/goharbor/harbor/src/replication/util" "github.com/goharbor/harbor/src/replication/util"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/replication/config"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
"github.com/goharbor/harbor/src/replication/operation" "github.com/goharbor/harbor/src/replication/operation"
"github.com/goharbor/harbor/src/replication/policy" "github.com/goharbor/harbor/src/replication/policy"
@ -159,8 +160,8 @@ func PopulateRegistries(registryMgr registry.Manager, policy *model.Policy) erro
} }
func getRegistry(registryMgr registry.Manager, registry *model.Registry) (*model.Registry, error) { func getRegistry(registryMgr registry.Manager, registry *model.Registry) (*model.Registry, error) {
if registry == nil { if registry == nil || registry.ID == 0 {
return nil, errors.New("empty registry") return GetLocalRegistry(), nil
} }
reg, err := registryMgr.Get(registry.ID) reg, err := registryMgr.Get(registry.ID)
if err != nil { if err != nil {
@ -171,3 +172,20 @@ func getRegistry(registryMgr registry.Manager, registry *model.Registry) (*model
} }
return reg, nil return reg, nil
} }
// GetLocalRegistry returns the info of the local Harbor registry
func GetLocalRegistry() *model.Registry {
return &model.Registry{
Type: model.RegistryTypeHarbor,
Name: "Local",
URL: config.Config.RegistryURL,
CoreURL: config.Config.CoreURL,
Status: "healthy",
Credential: &model.Credential{
Type: model.CredentialTypeSecret,
// use secret to do the auth for the local Harbor
AccessSecret: config.Config.JobserviceSecret,
},
Insecure: true,
}
}

View File

@ -62,12 +62,6 @@ func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Pol
ID: 1, ID: 1,
Enabled: true, Enabled: true,
Deletion: true, Deletion: true,
SrcRegistry: &model.Registry{
ID: 1,
},
DestRegistry: &model.Registry{
ID: 1,
},
Trigger: &model.Trigger{ Trigger: &model.Trigger{
Type: model.TriggerTypeEventBased, Type: model.TriggerTypeEventBased,
}, },
@ -83,13 +77,7 @@ func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Pol
ID: 2, ID: 2,
Enabled: true, Enabled: true,
Deletion: true, Deletion: true,
SrcRegistry: &model.Registry{ Trigger: nil,
ID: 1,
},
DestRegistry: &model.Registry{
ID: 1,
},
Trigger: nil,
Filters: []*model.Filter{ Filters: []*model.Filter{
{ {
Type: model.FilterTypeName, Type: model.FilterTypeName,
@ -102,12 +90,6 @@ func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Pol
ID: 3, ID: 3,
Enabled: true, Enabled: true,
Deletion: false, Deletion: false,
SrcRegistry: &model.Registry{
ID: 1,
},
DestRegistry: &model.Registry{
ID: 1,
},
Trigger: &model.Trigger{ Trigger: &model.Trigger{
Type: model.TriggerTypeEventBased, Type: model.TriggerTypeEventBased,
}, },
@ -123,12 +105,6 @@ func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Pol
ID: 4, ID: 4,
Enabled: true, Enabled: true,
Deletion: true, Deletion: true,
SrcRegistry: &model.Registry{
ID: 1,
},
DestRegistry: &model.Registry{
ID: 1,
},
Trigger: &model.Trigger{ Trigger: &model.Trigger{
Type: model.TriggerTypeEventBased, Type: model.TriggerTypeEventBased,
}, },
@ -144,12 +120,6 @@ func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Pol
ID: 5, ID: 5,
Enabled: false, Enabled: false,
Deletion: true, Deletion: true,
SrcRegistry: &model.Registry{
ID: 1,
},
DestRegistry: &model.Registry{
ID: 1,
},
Trigger: &model.Trigger{ Trigger: &model.Trigger{
Type: model.TriggerTypeEventBased, Type: model.TriggerTypeEventBased,
}, },

View File

@ -72,12 +72,20 @@ func (p *Policy) Valid(v *validation.Validation) {
if len(p.Name) == 0 { if len(p.Name) == 0 {
v.SetError("name", "cannot be empty") v.SetError("name", "cannot be empty")
} }
if p.SrcRegistry == nil || p.SrcRegistry.ID == 0 { var srcRegistryID, dstRegistryID int64
v.SetError("src_registry", "cannot be empty") if p.SrcRegistry != nil {
srcRegistryID = p.SrcRegistry.ID
} }
if p.DestRegistry == nil || p.DestRegistry.ID == 0 { if p.DestRegistry != nil {
v.SetError("dest_registry", "cannot be empty") dstRegistryID = p.DestRegistry.ID
} }
// one of the source registry and destination registry must be Harbor itself
if srcRegistryID != 0 && dstRegistryID != 0 ||
srcRegistryID == 0 && dstRegistryID == 0 {
v.SetError("src_registry, dest_registry", "one of them should be empty and the other one shouldn't be empty")
}
// valid the filters // valid the filters
for _, filter := range p.Filters { for _, filter := range p.Filters {
if filter.Type != FilterTypeResource && if filter.Type != FilterTypeResource &&
@ -88,6 +96,7 @@ func (p *Policy) Valid(v *validation.Validation) {
break break
} }
} }
// valid trigger // valid trigger
if p.Trigger != nil { if p.Trigger != nil {
if p.Trigger.Type != TriggerTypeManual && if p.Trigger.Type != TriggerTypeManual &&

View File

@ -31,20 +31,23 @@ func TestValidOfPolicy(t *testing.T) {
policy: &Policy{}, policy: &Policy{},
pass: false, pass: false,
}, },
// empty source registry // empty source registry and destination registry
{ {
policy: &Policy{ policy: &Policy{
Name: "policy01", Name: "policy01",
}, },
pass: false, pass: false,
}, },
// empty destination registry // source registry and destination registry both not empty
{ {
policy: &Policy{ policy: &Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &Registry{ SrcRegistry: &Registry{
ID: 1, ID: 1,
}, },
DestRegistry: &Registry{
ID: 2,
},
}, },
pass: false, pass: false,
}, },
@ -53,10 +56,10 @@ func TestValidOfPolicy(t *testing.T) {
policy: &Policy{ policy: &Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &Registry{ SrcRegistry: &Registry{
ID: 1, ID: 0,
}, },
DestRegistry: &Registry{ DestRegistry: &Registry{
ID: 2, ID: 1,
}, },
Filters: []*Filter{ Filters: []*Filter{
{ {
@ -71,10 +74,10 @@ func TestValidOfPolicy(t *testing.T) {
policy: &Policy{ policy: &Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &Registry{ SrcRegistry: &Registry{
ID: 1, ID: 0,
}, },
DestRegistry: &Registry{ DestRegistry: &Registry{
ID: 2, ID: 1,
}, },
Filters: []*Filter{ Filters: []*Filter{
{ {
@ -93,10 +96,10 @@ func TestValidOfPolicy(t *testing.T) {
policy: &Policy{ policy: &Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &Registry{ SrcRegistry: &Registry{
ID: 1, ID: 0,
}, },
DestRegistry: &Registry{ DestRegistry: &Registry{
ID: 2, ID: 1,
}, },
Filters: []*Filter{ Filters: []*Filter{
{ {
@ -115,10 +118,10 @@ func TestValidOfPolicy(t *testing.T) {
policy: &Policy{ policy: &Policy{
Name: "policy01", Name: "policy01",
SrcRegistry: &Registry{ SrcRegistry: &Registry{
ID: 1, ID: 0,
}, },
DestRegistry: &Registry{ DestRegistry: &Registry{
ID: 2, ID: 1,
}, },
Filters: []*Filter{ Filters: []*Filter{
{ {

View File

@ -23,10 +23,8 @@ import (
// const definition // const definition
const ( const (
// RegistryTypeHarbor indicates registry type harbor // RegistryTypeHarbor indicates registry type harbor
RegistryTypeHarbor RegistryType = "harbor" RegistryTypeHarbor RegistryType = "harbor"
// Local Harbor is the type of Harbor instance where the replication service is running on RegistryTypeDockerHub RegistryType = "dockerHub"
RegistryTypeLocalHarbor RegistryType = "localHarbor"
RegistryTypeDockerHub RegistryType = "dockerHub"
FilterStyleTypeText = "input" FilterStyleTypeText = "input"
FilterStyleTypeRadio = "radio" FilterStyleTypeRadio = "radio"
@ -96,7 +94,6 @@ type Registry struct {
type RegistryQuery struct { type RegistryQuery struct {
// Name is name of the registry to query // Name is name of the registry to query
Name string Name string
Type string
// Pagination specifies the pagination // Pagination specifies the pagination
Pagination *models.Pagination Pagination *models.Pagination
} }

View File

@ -91,7 +91,6 @@ func (m *DefaultManager) List(query ...*model.RegistryQuery) (int64, []*model.Re
// limit being -1 indicates no pagination specified, result in all registries matching name returned. // limit being -1 indicates no pagination specified, result in all registries matching name returned.
listQuery := &dao.ListRegistryQuery{ listQuery := &dao.ListRegistryQuery{
Query: query[0].Name, Query: query[0].Name,
Type: query[0].Type,
Limit: -1, Limit: -1,
} }
if query[0].Pagination != nil { if query[0].Pagination != nil {