Add support for namespace clone, export, import
This commit is contained in:
@@ -134,6 +134,53 @@ endpoints:
|
||||
type: "*multipart.FileHeader"
|
||||
required: true
|
||||
title: File to upload
|
||||
- name: clone
|
||||
path: "/{namespaceID}/clone"
|
||||
method: POST
|
||||
title: Clone compose namespace
|
||||
parameters:
|
||||
path:
|
||||
- type: uint64
|
||||
name: namespaceID
|
||||
required: true
|
||||
title: ID
|
||||
post:
|
||||
- type: string
|
||||
name: name
|
||||
required: true
|
||||
title: Duplicate name
|
||||
- type: string
|
||||
name: slug
|
||||
required: true
|
||||
title: Duplicate slug
|
||||
- name: export
|
||||
path: "/{namespaceID}/export/{filename}.zip"
|
||||
method: GET
|
||||
title: Export compose namespace
|
||||
parameters:
|
||||
path:
|
||||
- type: uint64
|
||||
name: namespaceID
|
||||
required: true
|
||||
title: ID
|
||||
- type: string
|
||||
name: filename
|
||||
required: true
|
||||
title: Output file name
|
||||
- type: string
|
||||
name: ext
|
||||
required: true
|
||||
title: Output file ext
|
||||
- name: import
|
||||
path: "/import"
|
||||
method: POST
|
||||
title: Import namespace
|
||||
parameters:
|
||||
post:
|
||||
- name: upload
|
||||
type: "*multipart.FileHeader"
|
||||
required: true
|
||||
title: Namespace import
|
||||
- name: triggerScript
|
||||
method: POST
|
||||
title: Fire compose:namespace trigger
|
||||
|
||||
@@ -25,6 +25,9 @@ type (
|
||||
Update(context.Context, *request.NamespaceUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.NamespaceDelete) (interface{}, error)
|
||||
Upload(context.Context, *request.NamespaceUpload) (interface{}, error)
|
||||
Clone(context.Context, *request.NamespaceClone) (interface{}, error)
|
||||
Export(context.Context, *request.NamespaceExport) (interface{}, error)
|
||||
Import(context.Context, *request.NamespaceImport) (interface{}, error)
|
||||
TriggerScript(context.Context, *request.NamespaceTriggerScript) (interface{}, error)
|
||||
ListTranslations(context.Context, *request.NamespaceListTranslations) (interface{}, error)
|
||||
UpdateTranslations(context.Context, *request.NamespaceUpdateTranslations) (interface{}, error)
|
||||
@@ -38,6 +41,9 @@ type (
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Upload func(http.ResponseWriter, *http.Request)
|
||||
Clone func(http.ResponseWriter, *http.Request)
|
||||
Export func(http.ResponseWriter, *http.Request)
|
||||
Import func(http.ResponseWriter, *http.Request)
|
||||
TriggerScript func(http.ResponseWriter, *http.Request)
|
||||
ListTranslations func(http.ResponseWriter, *http.Request)
|
||||
UpdateTranslations func(http.ResponseWriter, *http.Request)
|
||||
@@ -142,6 +148,54 @@ func NewNamespace(h NamespaceAPI) *Namespace {
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
Clone: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewNamespaceClone()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Clone(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
Export: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewNamespaceExport()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Export(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
Import: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewNamespaceImport()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Import(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
TriggerScript: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewNamespaceTriggerScript()
|
||||
@@ -202,6 +256,9 @@ func (h Namespace) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
|
||||
r.Post("/namespace/{namespaceID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}", h.Delete)
|
||||
r.Post("/namespace/upload", h.Upload)
|
||||
r.Post("/namespace/{namespaceID}/clone", h.Clone)
|
||||
r.Get("/namespace/{namespaceID}/export/{filename}.zip", h.Export)
|
||||
r.Post("/namespace/import", h.Import)
|
||||
r.Post("/namespace/{namespaceID}/trigger", h.TriggerScript)
|
||||
r.Get("/namespace/{namespaceID}/translation", h.ListTranslations)
|
||||
r.Patch("/namespace/{namespaceID}/translation", h.UpdateTranslations)
|
||||
|
||||
@@ -2,6 +2,10 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/compose/service"
|
||||
@@ -9,6 +13,10 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/corredor"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
|
||||
envoyStore "github.com/cortezaproject/corteza-server/pkg/envoy/store"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
|
||||
"github.com/cortezaproject/corteza-server/pkg/filter"
|
||||
)
|
||||
|
||||
@@ -163,6 +171,133 @@ func (ctrl Namespace) Upload(ctx context.Context, r *request.NamespaceUpload) (i
|
||||
return makeAttachmentPayload(ctx, a, err)
|
||||
}
|
||||
|
||||
func (ctrl Namespace) Clone(ctx context.Context, r *request.NamespaceClone) (interface{}, error) {
|
||||
dup := &types.Namespace{
|
||||
Name: r.Name,
|
||||
Slug: r.Slug,
|
||||
}
|
||||
|
||||
// prepare filters
|
||||
df := envoyStore.NewDecodeFilter()
|
||||
|
||||
// - compose resources
|
||||
df = df.ComposeNamespace(&types.NamespaceFilter{
|
||||
NamespaceID: []uint64{r.NamespaceID},
|
||||
}).
|
||||
ComposeModule(&types.ModuleFilter{}).
|
||||
ComposePage(&types.PageFilter{}).
|
||||
ComposeChart(&types.ChartFilter{})
|
||||
|
||||
// - workflow
|
||||
// @todo how do we want to handle these ones?
|
||||
// do we handle these ones?
|
||||
|
||||
decoder := func() (resource.InterfaceSet, error) {
|
||||
// get from store
|
||||
return envoyStore.Decoder().Decode(ctx, service.DefaultStore, df)
|
||||
}
|
||||
|
||||
encoder := func(nn resource.InterfaceSet) error {
|
||||
// prepare for encoding
|
||||
se := envoyStore.NewStoreEncoder(service.DefaultStore, &envoyStore.EncoderConfig{})
|
||||
bld := envoy.NewBuilder(se)
|
||||
g, err := bld.Build(ctx, nn...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return envoy.Encode(ctx, g, se)
|
||||
}
|
||||
|
||||
ns, err := ctrl.namespace.Clone(ctx, r.NamespaceID, dup, decoder, encoder)
|
||||
return ctrl.makePayload(ctx, ns, err)
|
||||
}
|
||||
|
||||
func (ctrl Namespace) Export(ctx context.Context, r *request.NamespaceExport) (interface{}, error) {
|
||||
var (
|
||||
// @todo support multiple archive types
|
||||
ext = "zip"
|
||||
file = fmt.Sprintf("%s.%s", r.Filename, ext)
|
||||
)
|
||||
|
||||
// prepare filters
|
||||
df := envoyStore.NewDecodeFilter()
|
||||
|
||||
// - compose resources
|
||||
df = df.ComposeNamespace(&types.NamespaceFilter{
|
||||
NamespaceID: []uint64{r.NamespaceID},
|
||||
}).
|
||||
ComposeModule(&types.ModuleFilter{}).
|
||||
ComposePage(&types.PageFilter{}).
|
||||
ComposeChart(&types.ChartFilter{})
|
||||
|
||||
// - workflow
|
||||
// @todo how do we want to handle these ones?
|
||||
// do we handle these ones?
|
||||
|
||||
decoder := func() (resource.InterfaceSet, error) {
|
||||
// get from store
|
||||
sd := envoyStore.Decoder()
|
||||
return sd.Decode(ctx, service.DefaultStore, df)
|
||||
}
|
||||
|
||||
encoder := func(nn resource.InterfaceSet) (envoy.Streamer, error) {
|
||||
// prepare for encoding
|
||||
ye := yaml.NewYamlEncoder(&yaml.EncoderConfig{})
|
||||
bld := envoy.NewBuilder(ye)
|
||||
g, err := bld.Build(ctx, nn...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = envoy.Encode(ctx, g, ye)
|
||||
return ye, err
|
||||
}
|
||||
|
||||
rs, err := ctrl.namespace.Export(ctx, r.NamespaceID, ext, decoder, encoder)
|
||||
return ctrl.serveExport(ctx, file, rs, err)
|
||||
}
|
||||
|
||||
func (ctrl Namespace) Import(ctx context.Context, r *request.NamespaceImport) (interface{}, error) {
|
||||
f, err := r.Upload.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := func(nn resource.InterfaceSet) error {
|
||||
se := envoyStore.NewStoreEncoder(service.DefaultStore, &envoyStore.EncoderConfig{})
|
||||
|
||||
bld := envoy.NewBuilder(se)
|
||||
g, err := bld.Build(ctx, nn...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = envoy.Encode(ctx, g, se)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ns, err := ctrl.namespace.Import(ctx, f, r.Upload.Size, encoder)
|
||||
return ctrl.makePayload(ctx, ns, err)
|
||||
}
|
||||
|
||||
func (ctrl Namespace) serveExport(ctx context.Context, fn string, archive io.ReadSeeker, err error) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Add("Content-Disposition", "attachment; filename="+fn)
|
||||
|
||||
http.ServeContent(w, req, fn, time.Now(), archive)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl *Namespace) TriggerScript(ctx context.Context, r *request.NamespaceTriggerScript) (rsp interface{}, err error) {
|
||||
var (
|
||||
namespace *types.Namespace
|
||||
|
||||
@@ -154,6 +154,47 @@ type (
|
||||
Upload *multipart.FileHeader
|
||||
}
|
||||
|
||||
NamespaceClone struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
// ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// Name POST parameter
|
||||
//
|
||||
// Duplicate name
|
||||
Name string
|
||||
|
||||
// Slug POST parameter
|
||||
//
|
||||
// Duplicate slug
|
||||
Slug string
|
||||
}
|
||||
|
||||
NamespaceExport struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
// ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// Filename PATH parameter
|
||||
//
|
||||
// Output file name
|
||||
Filename string
|
||||
|
||||
// Ext PATH parameter
|
||||
//
|
||||
// Output file ext
|
||||
Ext string
|
||||
}
|
||||
|
||||
NamespaceImport struct {
|
||||
// Upload POST parameter
|
||||
//
|
||||
// Namespace import
|
||||
Upload *multipart.FileHeader
|
||||
}
|
||||
|
||||
NamespaceTriggerScript struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
@@ -646,6 +687,192 @@ func (r *NamespaceUpload) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// NewNamespaceClone request
|
||||
func NewNamespaceClone() *NamespaceClone {
|
||||
return &NamespaceClone{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceClone) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"name": r.Name,
|
||||
"slug": r.Slug,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceClone) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceClone) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceClone) GetSlug() string {
|
||||
return r.Slug
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *NamespaceClone) Fill(req *http.Request) (err error) {
|
||||
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("error parsing http request body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// POST params
|
||||
|
||||
if val, ok := req.Form["name"]; ok && len(val) > 0 {
|
||||
r.Name, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.Form["slug"]; ok && len(val) > 0 {
|
||||
r.Slug, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "namespaceID")
|
||||
r.NamespaceID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewNamespaceExport request
|
||||
func NewNamespaceExport() *NamespaceExport {
|
||||
return &NamespaceExport{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceExport) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"filename": r.Filename,
|
||||
"ext": r.Ext,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceExport) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceExport) GetFilename() string {
|
||||
return r.Filename
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceExport) GetExt() string {
|
||||
return r.Ext
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *NamespaceExport) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "namespaceID")
|
||||
r.NamespaceID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = chi.URLParam(req, "filename")
|
||||
r.Filename, err = val, nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = chi.URLParam(req, "ext")
|
||||
r.Ext, err = val, nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewNamespaceImport request
|
||||
func NewNamespaceImport() *NamespaceImport {
|
||||
return &NamespaceImport{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceImport) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"upload": r.Upload,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r NamespaceImport) GetUpload() *multipart.FileHeader {
|
||||
return r.Upload
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *NamespaceImport) Fill(req *http.Request) (err error) {
|
||||
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("error parsing http request body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// POST params
|
||||
|
||||
if _, r.Upload, err = req.FormFile("upload"); err != nil {
|
||||
return fmt.Errorf("error processing uploaded file: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewNamespaceTriggerScript request
|
||||
func NewNamespaceTriggerScript() *NamespaceTriggerScript {
|
||||
return &NamespaceTriggerScript{}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
automationTypes "github.com/cortezaproject/corteza-server/automation/types"
|
||||
"github.com/cortezaproject/corteza-server/compose/service/event"
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza-server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza-server/pkg/handle"
|
||||
@@ -15,6 +25,7 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
@@ -22,9 +33,13 @@ type (
|
||||
namespace struct {
|
||||
actionlog actionlog.Recorder
|
||||
ac namespaceAccessController
|
||||
eventbus eventDispatcher
|
||||
store store.Storer
|
||||
locale ResourceTranslationsManagerService
|
||||
modAc moduleAccessController
|
||||
pageAc pageAccessController
|
||||
chartAc chartAccessController
|
||||
|
||||
eventbus eventDispatcher
|
||||
store store.Storer
|
||||
locale ResourceTranslationsManagerService
|
||||
}
|
||||
|
||||
namespaceAccessController interface {
|
||||
@@ -45,6 +60,9 @@ type (
|
||||
|
||||
Create(ctx context.Context, namespace *types.Namespace) (*types.Namespace, error)
|
||||
Update(ctx context.Context, namespace *types.Namespace) (*types.Namespace, error)
|
||||
Clone(ctx context.Context, namespaceID uint64, dup *types.Namespace, decoder func() (resource.InterfaceSet, error), encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error)
|
||||
Export(ctx context.Context, namespaceID uint64, archive string, decoder func() (resource.InterfaceSet, error), encoder func(resource.InterfaceSet) (envoy.Streamer, error)) (r io.ReadSeeker, err error)
|
||||
Import(ctx context.Context, f multipart.File, size int64, encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error)
|
||||
DeleteByID(ctx context.Context, namespaceID uint64) error
|
||||
}
|
||||
|
||||
@@ -60,7 +78,11 @@ const (
|
||||
|
||||
func Namespace() *namespace {
|
||||
return &namespace{
|
||||
ac: DefaultAccessControl,
|
||||
ac: DefaultAccessControl,
|
||||
modAc: DefaultAccessControl,
|
||||
pageAc: DefaultAccessControl,
|
||||
chartAc: DefaultAccessControl,
|
||||
|
||||
eventbus: eventbus.Service(),
|
||||
actionlog: DefaultActionlog,
|
||||
store: DefaultStore,
|
||||
@@ -234,6 +256,245 @@ func (svc namespace) Update(ctx context.Context, upd *types.Namespace) (c *types
|
||||
return svc.updater(ctx, upd.ID, NamespaceActionUpdate, svc.handleUpdate(ctx, upd))
|
||||
}
|
||||
|
||||
func (svc namespace) Clone(ctx context.Context, namespaceID uint64, dup *types.Namespace, decoder func() (resource.InterfaceSet, error), encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error) {
|
||||
var (
|
||||
aProps = &namespaceActionProps{namespace: dup}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
// Preparation
|
||||
// - target namespace
|
||||
targetNs, err := loadNamespace(ctx, svc.store, namespaceID)
|
||||
if errors.IsNotFound(err) {
|
||||
return NamespaceErrNotFound()
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
aProps.setNamespace(targetNs)
|
||||
|
||||
// - destination namespace
|
||||
dstNs, err := store.LookupComposeNamespaceBySlug(ctx, svc.store, dup.Slug)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
if dstNs != nil {
|
||||
return NamespaceErrHandleNotUnique()
|
||||
}
|
||||
|
||||
// Access control
|
||||
if err = svc.canExport(ctx, targetNs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get namespace resources
|
||||
nn, err := decoder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// some meta bits
|
||||
sNsID := strconv.FormatUint(namespaceID, 10)
|
||||
oldNsRef := resource.MakeRef(types.NamespaceResourceType, resource.MakeIdentifiers(sNsID))
|
||||
newNsRef := resource.MakeRef(types.NamespaceResourceType, resource.MakeIdentifiers(dup.Slug, dup.Name))
|
||||
prune := resource.RefSet{resource.MakeWildRef(automationTypes.WorkflowResourceType)}
|
||||
|
||||
// rename the namespace
|
||||
//
|
||||
// For now we will find the namespace in set and change it's name, handle.
|
||||
// The rest of the resources can stay as are.
|
||||
//
|
||||
// @todo add a more flexible system for such modifications
|
||||
auxNs := resource.FindComposeNamespace(nn, oldNsRef.Identifiers)
|
||||
auxNs.ID = 0
|
||||
auxNs.Name = dup.Name
|
||||
auxNs.Slug = dup.Slug
|
||||
dup = auxNs
|
||||
aProps.setNamespace(dup)
|
||||
|
||||
// Correct internal references
|
||||
// - namespace identifiers
|
||||
nn.SearchForIdentifiers(oldNsRef.Identifiers).Walk(func(r resource.Interface) error {
|
||||
r.ReID(newNsRef.Identifiers)
|
||||
return nil
|
||||
})
|
||||
|
||||
// - relations
|
||||
nn.SearchForReferences(oldNsRef).Walk(func(r resource.Interface) error {
|
||||
r.ReRef(resource.RefSet{oldNsRef}, resource.RefSet{newNsRef})
|
||||
|
||||
// - additional pruning
|
||||
pp, ok := r.(resource.PrunableInterface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range prune {
|
||||
pp.Prune(p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// encode
|
||||
return encoder(nn)
|
||||
}()
|
||||
|
||||
return dup, svc.recordAction(ctx, aProps, NamespaceActionClone, err)
|
||||
}
|
||||
|
||||
func (svc namespace) Export(ctx context.Context, namespaceID uint64, archive string, decoder func() (resource.InterfaceSet, error), encoder func(resource.InterfaceSet) (envoy.Streamer, error)) (r io.ReadSeeker, err error) {
|
||||
var (
|
||||
aProps = &namespaceActionProps{archiveFormat: archive}
|
||||
)
|
||||
|
||||
// make archive
|
||||
buf := bytes.NewBuffer(nil)
|
||||
w := zip.NewWriter(buf)
|
||||
|
||||
err = func() error {
|
||||
if archive != "zip" {
|
||||
return NamespaceErrUnsupportedExportFormat()
|
||||
}
|
||||
|
||||
// initial validation
|
||||
// - target namespace
|
||||
targetNs, err := store.LookupComposeNamespaceByID(ctx, svc.store, namespaceID)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
aProps.setNamespace(targetNs)
|
||||
|
||||
// - ac
|
||||
if err = svc.canExport(ctx, targetNs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get namespace resources
|
||||
nn, err := decoder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// some meta bits
|
||||
sNsID := strconv.FormatUint(namespaceID, 10)
|
||||
oldNsRef := resource.MakeRef(types.NamespaceResourceType, resource.MakeIdentifiers(sNsID))
|
||||
prune := resource.RefSet{resource.MakeWildRef(automationTypes.WorkflowResourceType)}
|
||||
|
||||
// - prune resources we won't preserve
|
||||
nn.SearchForReferences(oldNsRef).Walk(func(r resource.Interface) error {
|
||||
pp, ok := r.(resource.PrunableInterface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range prune {
|
||||
pp.Prune(p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// encode
|
||||
ss, err := encoder(nn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create archive
|
||||
for _, s := range ss.Stream() {
|
||||
// @todo generalize when needed
|
||||
f, err := w.Create(fmt.Sprintf("%s.yaml", s.Resource))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bb, err := ioutil.ReadAll(s.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(bb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}()
|
||||
|
||||
return bytes.NewReader(buf.Bytes()), svc.recordAction(ctx, aProps, NamespaceActionExport, err)
|
||||
}
|
||||
|
||||
func (svc namespace) Import(ctx context.Context, f multipart.File, size int64, encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error) {
|
||||
var (
|
||||
aProps = &namespaceActionProps{}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
// access control
|
||||
if err := svc.canImport(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// archive type check
|
||||
mt, err := mimetype.DetectReader(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aProps.setArchiveFormat(mt.Extension())
|
||||
if !mt.Is("application/zip") {
|
||||
return NamespaceErrUnsupportedImportFormat()
|
||||
}
|
||||
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// un-archive
|
||||
archive, err := zip.NewReader(f, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode with Envoy
|
||||
yd := yaml.Decoder()
|
||||
nn := make([]resource.Interface, 0, 10)
|
||||
|
||||
for _, f := range archive.File {
|
||||
a, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.Close()
|
||||
|
||||
mm, err := yd.Decode(ctx, a, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nn = append(nn, mm...)
|
||||
}
|
||||
|
||||
// encode
|
||||
err = encoder(nn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find the ns node
|
||||
for _, n := range nn {
|
||||
if nsn, ok := n.(*resource.ComposeNamespace); ok {
|
||||
ns = nsn.Res
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
aProps.setNamespace(ns)
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return ns, svc.recordAction(ctx, aProps, NamespaceActionImport, err)
|
||||
}
|
||||
|
||||
func (svc namespace) DeleteByID(ctx context.Context, namespaceID uint64) error {
|
||||
return trim1st(svc.updater(ctx, namespaceID, NamespaceActionDelete, svc.handleDelete))
|
||||
}
|
||||
@@ -430,6 +691,64 @@ func (svc namespace) handleUndelete(ctx context.Context, ns *types.Namespace) (n
|
||||
return namespaceChanged, nil
|
||||
}
|
||||
|
||||
func (svc namespace) canExport(ctx context.Context, namespace *types.Namespace) error {
|
||||
// Preload all of the relevant stuff for access control
|
||||
// - modules
|
||||
// no need to load fields
|
||||
mm, _, err := store.SearchComposeModules(ctx, svc.store, types.ModuleFilter{NamespaceID: namespace.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// - pages
|
||||
pp, _, err := store.SearchComposePages(ctx, svc.store, types.PageFilter{NamespaceID: namespace.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// - charts
|
||||
cc, _, err := store.SearchComposeCharts(ctx, svc.store, types.ChartFilter{NamespaceID: namespace.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// access control
|
||||
// - namespace
|
||||
if !svc.ac.CanReadNamespace(ctx, namespace) {
|
||||
return NamespaceErrNotAllowedToRead()
|
||||
}
|
||||
// - modules
|
||||
for _, m := range mm {
|
||||
if !svc.modAc.CanReadModule(ctx, m) {
|
||||
return ModuleErrNotAllowedToRead()
|
||||
}
|
||||
}
|
||||
// - pages
|
||||
for _, p := range pp {
|
||||
if !svc.pageAc.CanReadPage(ctx, p) {
|
||||
return PageErrNotAllowedToRead()
|
||||
}
|
||||
}
|
||||
// - charts
|
||||
for _, c := range cc {
|
||||
if !svc.chartAc.CanReadChart(ctx, c) {
|
||||
return ChartErrNotAllowedToRead()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc namespace) canImport(ctx context.Context) error {
|
||||
|
||||
// If a user is allowed to create a namespace, they are considered to be allowed
|
||||
// to create any underlying resource when it comes to importing.
|
||||
//
|
||||
// This was agreed upon internally and may change in the future.
|
||||
|
||||
if !svc.ac.CanCreateNamespace(ctx) {
|
||||
return NamespaceErrNotAllowedToCreate()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadNamespace(ctx context.Context, s store.Storer, namespaceID uint64) (ns *types.Namespace, err error) {
|
||||
if namespaceID == 0 {
|
||||
return nil, ChartErrInvalidNamespaceID()
|
||||
|
||||
Generated
+185
-3
@@ -21,9 +21,10 @@ import (
|
||||
|
||||
type (
|
||||
namespaceActionProps struct {
|
||||
namespace *types.Namespace
|
||||
changed *types.Namespace
|
||||
filter *types.NamespaceFilter
|
||||
namespace *types.Namespace
|
||||
changed *types.Namespace
|
||||
archiveFormat string
|
||||
filter *types.NamespaceFilter
|
||||
}
|
||||
|
||||
namespaceAction struct {
|
||||
@@ -73,6 +74,17 @@ func (p *namespaceActionProps) setChanged(changed *types.Namespace) *namespaceAc
|
||||
return p
|
||||
}
|
||||
|
||||
// setArchiveFormat updates namespaceActionProps's archiveFormat
|
||||
//
|
||||
// Allows method chaining
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (p *namespaceActionProps) setArchiveFormat(archiveFormat string) *namespaceActionProps {
|
||||
p.archiveFormat = archiveFormat
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates namespaceActionProps's filter
|
||||
//
|
||||
// Allows method chaining
|
||||
@@ -106,6 +118,7 @@ func (p namespaceActionProps) Serialize() actionlog.Meta {
|
||||
m.Set("changed.meta", p.changed.Meta, true)
|
||||
m.Set("changed.enabled", p.changed.Enabled, true)
|
||||
}
|
||||
m.Set("archiveFormat", p.archiveFormat, true)
|
||||
if p.filter != nil {
|
||||
m.Set("filter.query", p.filter.Query, true)
|
||||
m.Set("filter.slug", p.filter.Slug, true)
|
||||
@@ -178,6 +191,7 @@ func (p namespaceActionProps) Format(in string, err error) string {
|
||||
pairs = append(pairs, "{{changed.meta}}", fns(p.changed.Meta))
|
||||
pairs = append(pairs, "{{changed.enabled}}", fns(p.changed.Enabled))
|
||||
}
|
||||
pairs = append(pairs, "{{archiveFormat}}", fns(p.archiveFormat))
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
@@ -311,6 +325,66 @@ func NamespaceActionUpdate(props ...*namespaceActionProps) *namespaceAction {
|
||||
return a
|
||||
}
|
||||
|
||||
// NamespaceActionClone returns "compose:namespace.clone" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func NamespaceActionClone(props ...*namespaceActionProps) *namespaceAction {
|
||||
a := &namespaceAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "compose:namespace",
|
||||
action: "clone",
|
||||
log: "cloned {namespace}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NamespaceActionExport returns "compose:namespace.export" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func NamespaceActionExport(props ...*namespaceActionProps) *namespaceAction {
|
||||
a := &namespaceAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "compose:namespace",
|
||||
action: "export",
|
||||
log: "exported {namespace}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NamespaceActionImport returns "compose:namespace.import" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func NamespaceActionImport(props ...*namespaceActionProps) *namespaceAction {
|
||||
a := &namespaceAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "compose:namespace",
|
||||
action: "import",
|
||||
log: "imported {namespace}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NamespaceActionDelete returns "compose:namespace.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
@@ -583,6 +657,114 @@ func NamespaceErrStaleData(mm ...*namespaceActionProps) *errors.Error {
|
||||
return e
|
||||
}
|
||||
|
||||
// NamespaceErrUnsupportedExportFormat returns "compose:namespace.unsupportedExportFormat" as *errors.Error
|
||||
//
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func NamespaceErrUnsupportedExportFormat(mm ...*namespaceActionProps) *errors.Error {
|
||||
var p = &namespaceActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("unsupported export format", nil),
|
||||
|
||||
errors.Meta("type", "unsupportedExportFormat"),
|
||||
errors.Meta("resource", "compose:namespace"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(namespaceLogMetaKey{}, "could not export namespace {{namespace}}; unsupported format {{archiveFormat}}"),
|
||||
errors.Meta(namespacePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "namespace.errors.unsupportedExportFormat"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NamespaceErrUnsupportedImportFormat returns "compose:namespace.unsupportedImportFormat" as *errors.Error
|
||||
//
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func NamespaceErrUnsupportedImportFormat(mm ...*namespaceActionProps) *errors.Error {
|
||||
var p = &namespaceActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("unsupported import format", nil),
|
||||
|
||||
errors.Meta("type", "unsupportedImportFormat"),
|
||||
errors.Meta("resource", "compose:namespace"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(namespaceLogMetaKey{}, "could not import namespace {{namespace}}; unsupported format {{archiveFormat}}"),
|
||||
errors.Meta(namespacePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "namespace.errors.unsupportedImportFormat"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NamespaceErrCloneMultiple returns "compose:namespace.cloneMultiple" as *errors.Error
|
||||
//
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func NamespaceErrCloneMultiple(mm ...*namespaceActionProps) *errors.Error {
|
||||
var p = &namespaceActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to clone multiple namespaces at once", nil),
|
||||
|
||||
errors.Meta("type", "cloneMultiple"),
|
||||
errors.Meta("resource", "compose:namespace"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(namespaceLogMetaKey{}, "could not clone namespaces; multiple duplications requested at once"),
|
||||
errors.Meta(namespacePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "namespace.errors.cloneMultiple"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NamespaceErrNotAllowedToRead returns "compose:namespace.notAllowedToRead" as *errors.Error
|
||||
//
|
||||
//
|
||||
|
||||
@@ -19,6 +19,8 @@ props:
|
||||
- name: changed
|
||||
type: "*types.Namespace"
|
||||
fields: [ name, slug, ID, meta, enabled ]
|
||||
- name: archiveFormat
|
||||
type: string
|
||||
- name: filter
|
||||
type: "*types.NamespaceFilter"
|
||||
fields: [ query, slug, sort, limit ]
|
||||
@@ -38,6 +40,15 @@ actions:
|
||||
- action: update
|
||||
log: "updated {{namespace}}"
|
||||
|
||||
- action: clone
|
||||
log: "cloned {namespace}"
|
||||
|
||||
- action: export
|
||||
log: "exported {namespace}"
|
||||
|
||||
- action: import
|
||||
log: "imported {namespace}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{namespace}}"
|
||||
|
||||
@@ -69,6 +80,18 @@ errors:
|
||||
message: "stale data"
|
||||
severity: warning
|
||||
|
||||
- error: unsupportedExportFormat
|
||||
message: "unsupported export format"
|
||||
log: "could not export namespace {{namespace}}; unsupported format {{archiveFormat}}"
|
||||
|
||||
- error: unsupportedImportFormat
|
||||
message: "unsupported import format"
|
||||
log: "could not import namespace {{namespace}}; unsupported format {{archiveFormat}}"
|
||||
|
||||
- error: cloneMultiple
|
||||
message: "not allowed to clone multiple namespaces at once"
|
||||
log: "could not clone namespaces; multiple duplications requested at once"
|
||||
|
||||
- error: notAllowedToRead
|
||||
message: "not allowed to read this namespace"
|
||||
log: "could not read {{namespace}}; insufficient permissions"
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -55,6 +56,21 @@ func (opt ModuleFieldOptions) Bool(key string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (opt ModuleFieldOptions) UInt64(key string) uint64 {
|
||||
return opt.UInt64Def(key, 0)
|
||||
}
|
||||
|
||||
func (opt ModuleFieldOptions) UInt64Def(key string, def uint64) uint64 {
|
||||
if val, has := opt[key]; has {
|
||||
v, err := cast.ToUint64E(val)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (opt ModuleFieldOptions) Int64(key string) int64 {
|
||||
return opt.Int64Def(key, 0)
|
||||
}
|
||||
|
||||
@@ -114,10 +114,10 @@ func NewComposePage(pg *types.Page, nsRef, modRef, parentRef string) *ComposePag
|
||||
}
|
||||
|
||||
case "Comment":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id != "" {
|
||||
ref := r.AddRef(types.ModuleResourceType, id).Constraint(r.RefNs)
|
||||
r.BlockRefs[i] = add(r.BlockRefs[i], ref)
|
||||
ref = r.pbComment(b.Options)
|
||||
if ref != nil {
|
||||
r.addRef(ref.Constraint(r.RefNs))
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.ModRefs = append(r.ModRefs, ref)
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,15 @@ func (r *ComposePage) pbRecordList(opt map[string]interface{}) (out *Ref) {
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbComment(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbAutomation(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "workflow", "workflowID")
|
||||
if id == "" {
|
||||
|
||||
@@ -26,10 +26,6 @@ func (n *apiGateway) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
}
|
||||
|
||||
func (n *apiGateway) prepareRoute(ctx context.Context, pl *payload) (err error) {
|
||||
if n.cfg.IgnoreStore {
|
||||
n.res.Res.ID = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get the original workflow
|
||||
n.gwr, err = findAPIGatewayStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
@@ -48,13 +44,6 @@ func (n *apiGateway) prepareFilters(ctx context.Context, pl *payload) (err error
|
||||
return nil
|
||||
}
|
||||
|
||||
if n.cfg.IgnoreStore {
|
||||
for _, t := range n.ff {
|
||||
t.ID = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to find any related filters for this route
|
||||
tt, _, err := store.SearchApigwFilters(ctx, pl.s, types.ApigwFilterFilter{
|
||||
RouteID: n.gwr.ID,
|
||||
|
||||
@@ -17,11 +17,6 @@ func newReportFromResource(res *resource.Report, cfg *EncoderConfig) resourceSta
|
||||
}
|
||||
|
||||
func (n *report) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
if n.cfg.IgnoreStore {
|
||||
n.res.Res.ID = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get the original report
|
||||
n.rp, err = findReportStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
|
||||
@@ -743,7 +743,7 @@ func (df *DecodeFilter) RbacStrict(f *rbac.RuleFilter) *DecodeFilter {
|
||||
return df
|
||||
}
|
||||
|
||||
df.rbac = append(df.rbac, &rbacFilter{RuleFilter: *f, strict: true})
|
||||
df.rbac = append(df.rbac, &rbacFilter{RuleFilter: *f})
|
||||
return df
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
"github.com/cortezaproject/corteza-server/tests/helpers"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func fetchEntireNamespace(ctx context.Context, s store.Storer, slug string) (ns *types.Namespace, mm types.ModuleSet, pp types.PageSet, cc types.ChartSet, slg string, err error) {
|
||||
slg = slug
|
||||
|
||||
ns, err = store.LookupComposeNamespaceBySlug(ctx, s, slug)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mm, _, err = store.SearchComposeModules(ctx, s, types.ModuleFilter{NamespaceID: ns.ID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < len(mm); i++ {
|
||||
mm[i].Fields, _, err = store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ModuleID: []uint64{mm[i].ID}})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pp, _, err = store.SearchComposePages(ctx, s, types.PageFilter{NamespaceID: ns.ID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cc, _, err = store.SearchComposeCharts(ctx, s, types.ChartFilter{NamespaceID: ns.ID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func findModuleByHandle(mm types.ModuleSet, h string) *types.Module {
|
||||
for _, m := range mm {
|
||||
if m.Handle == h {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findChartByHandle(cc types.ChartSet, h string) *types.Chart {
|
||||
for _, m := range cc {
|
||||
if m.Handle == h {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPageByHandle(pp types.PageSet, h string) *types.Page {
|
||||
for _, m := range pp {
|
||||
if m.Handle == h {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test0001_namespace_duplicate(t *testing.T) {
|
||||
ctx, h, s := setup(t)
|
||||
loadScenario(ctx, defStore, t, h)
|
||||
ns, _, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1")
|
||||
h.a.NoError(err)
|
||||
|
||||
helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create")
|
||||
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
|
||||
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
|
||||
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
|
||||
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
|
||||
|
||||
h.apiInit().
|
||||
Post(fmt.Sprintf("/namespace/%d/clone", ns.ID)).
|
||||
JSON(`{ "slug": "cloned", "name": "cloned name" }`).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Assert(helpers.AssertNoErrors).
|
||||
End()
|
||||
|
||||
usedIDs := make(map[uint64]bool)
|
||||
checkID := func(h helper, id uint64) {
|
||||
if usedIDs[id] {
|
||||
h.a.FailNow(fmt.Sprintf("the ID is not unique across cloned resources: %d", id))
|
||||
}
|
||||
usedIDs[id] = true
|
||||
}
|
||||
|
||||
checker := func(ns *types.Namespace, mm types.ModuleSet, pp types.PageSet, cc types.ChartSet, slug string, err error) {
|
||||
h.a.NoError(err)
|
||||
|
||||
// NS
|
||||
h.a.Equal(slug, ns.Slug)
|
||||
|
||||
// Modules
|
||||
mod1 := findModuleByHandle(mm, "mod1")
|
||||
h.a.NotNil(mod1)
|
||||
checkID(h, mod1.ID)
|
||||
mod2 := findModuleByHandle(mm, "mod2")
|
||||
h.a.NotNil(mod2)
|
||||
checkID(h, mod2.ID)
|
||||
mod3 := findModuleByHandle(mm, "mod3")
|
||||
h.a.NotNil(mod3)
|
||||
checkID(h, mod3.ID)
|
||||
|
||||
h.a.Len(mod1.Fields, 4)
|
||||
h.a.Equal("Record", mod1.Fields[3].Kind)
|
||||
h.a.Equal(mod2.ID, mod1.Fields[3].Options.UInt64("moduleID"))
|
||||
|
||||
h.a.Len(mod2.Fields, 4)
|
||||
h.a.Equal("Record", mod2.Fields[1].Kind)
|
||||
h.a.Equal(mod2.ID, mod2.Fields[1].Options.UInt64("moduleID"))
|
||||
h.a.Equal("Record", mod2.Fields[3].Kind)
|
||||
h.a.Equal(mod3.ID, mod2.Fields[3].Options.UInt64("moduleID"))
|
||||
|
||||
h.a.Len(mod3.Fields, 1)
|
||||
|
||||
// Charts
|
||||
chr1 := findChartByHandle(cc, "chr1")
|
||||
checkID(h, chr1.ID)
|
||||
h.a.NotNil(chr1)
|
||||
h.a.Len(chr1.Config.Reports, 1)
|
||||
h.a.Equal(mod1.ID, chr1.Config.Reports[0].ModuleID)
|
||||
|
||||
// Pages
|
||||
pg1 := findPageByHandle(pp, "pg1")
|
||||
checkID(h, pg1.ID)
|
||||
h.a.NotNil(pg1)
|
||||
rpg2 := findPageByHandle(pp, "rpg2")
|
||||
checkID(h, rpg2.ID)
|
||||
h.a.NotNil(rpg2)
|
||||
|
||||
h.a.Len(pg1.Blocks, 3)
|
||||
|
||||
h.a.Equal("RecordList", pg1.Blocks[1].Kind)
|
||||
h.a.Equal(mod1.ID, cast.ToUint64(pg1.Blocks[1].Options["moduleID"]))
|
||||
h.a.Equal("Chart", pg1.Blocks[2].Kind)
|
||||
h.a.Equal(chr1.ID, cast.ToUint64(pg1.Blocks[2].Options["chartID"]))
|
||||
|
||||
h.a.Equal(rpg2.ModuleID, mod1.ID)
|
||||
}
|
||||
|
||||
checker(fetchEntireNamespace(ctx, s, "ns1"))
|
||||
checker(fetchEntireNamespace(ctx, s, "cloned"))
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
@@ -12,12 +13,19 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/api/server"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/csv"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/directory"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
|
||||
envoyStore "github.com/cortezaproject/corteza-server/pkg/envoy/store"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
|
||||
"github.com/cortezaproject/corteza-server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/objstore/plain"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rand"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
sysTypes "github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/cortezaproject/corteza-server/tests/helpers"
|
||||
"github.com/go-chi/chi"
|
||||
@@ -42,6 +50,7 @@ var (
|
||||
r chi.Router
|
||||
|
||||
eventBus = eventbus.New()
|
||||
defStore store.Storer
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -64,6 +73,7 @@ func InitTestApp() {
|
||||
|
||||
testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) {
|
||||
service.DefaultStore = app.Store
|
||||
defStore = app.Store
|
||||
service.DefaultObjectStore, err = plain.NewWithAfero(afero.NewMemMapFs(), "test")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -144,3 +154,86 @@ func (h helper) noError(err error) {
|
||||
|
||||
h.a.NoError(err)
|
||||
}
|
||||
|
||||
func collect(ee ...error) error {
|
||||
for _, e := range ee {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(t *testing.T) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
err := collect(
|
||||
defStore.TruncateComposeNamespaces(ctx),
|
||||
defStore.TruncateComposePages(ctx),
|
||||
defStore.TruncateComposeModuleFields(ctx),
|
||||
defStore.TruncateComposeModules(ctx),
|
||||
defStore.TruncateComposeRecords(ctx, nil),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode scenario data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadScenario(ctx context.Context, s store.Storer, t *testing.T, h helper) {
|
||||
loadScenarioWithName(ctx, s, t, h, "S"+t.Name()[4:])
|
||||
}
|
||||
|
||||
func loadScenarioWithName(ctx context.Context, s store.Storer, t *testing.T, h helper, scenario string) {
|
||||
cleanup(t)
|
||||
parseEnvoy(ctx, s, h, path.Join("testdata", scenario, "data_model"))
|
||||
}
|
||||
|
||||
func parseEnvoy(ctx context.Context, s store.Storer, h helper, path string) {
|
||||
nn, err := directory.Decode(
|
||||
ctx,
|
||||
path,
|
||||
yaml.Decoder(),
|
||||
csv.Decoder(),
|
||||
)
|
||||
if err != nil {
|
||||
h.t.Fatalf("failed to decode scenario data: %v", err)
|
||||
}
|
||||
|
||||
crs := resource.ComposeRecordShaper()
|
||||
nn, err = resource.Shape(nn, crs)
|
||||
h.a.NoError(err)
|
||||
|
||||
// import into the store
|
||||
se := envoyStore.NewStoreEncoder(s, nil)
|
||||
bld := envoy.NewBuilder(se)
|
||||
g, err := bld.Build(ctx, nn...)
|
||||
h.a.NoError(err)
|
||||
err = envoy.Encode(ctx, g, se)
|
||||
h.a.NoError(err)
|
||||
}
|
||||
|
||||
func bypassRBAC(ctx context.Context) context.Context {
|
||||
u := &sysTypes.User{
|
||||
ID: id.Next(),
|
||||
}
|
||||
|
||||
u.SetRoles(auth.BypassRoles().IDs()...)
|
||||
|
||||
return auth.SetIdentityToContext(ctx, u)
|
||||
}
|
||||
|
||||
func setup(t *testing.T) (context.Context, helper, store.Storer) {
|
||||
h := newHelper(t)
|
||||
s := service.DefaultStore
|
||||
|
||||
u := &sysTypes.User{
|
||||
ID: id.Next(),
|
||||
}
|
||||
u.SetRoles(auth.BypassRoles().IDs()...)
|
||||
|
||||
ctx := auth.SetIdentityToContext(context.Background(), u)
|
||||
|
||||
return ctx, h, s
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespaces:
|
||||
ns1:
|
||||
name: ns1 name
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Provides a complete set of things to cover regarding modules and module fields.
|
||||
# mod1: defines more complicated fields, relates to mod2
|
||||
# mod2: defines a self-ref, relates to mod3
|
||||
# mod3: just a placeholder
|
||||
|
||||
namespace: ns1
|
||||
modules:
|
||||
mod1:
|
||||
name: mod1 name
|
||||
fields:
|
||||
f1:
|
||||
label: f1 label
|
||||
kind: String
|
||||
required: true
|
||||
f2:
|
||||
label: f2 label
|
||||
kind: Select
|
||||
options:
|
||||
options:
|
||||
- f2 opt 1
|
||||
- f2 opt 2
|
||||
- f2 opt 3
|
||||
f3:
|
||||
label: f3 label
|
||||
kind: Select
|
||||
options:
|
||||
options:
|
||||
- ☆☆☆☆☆
|
||||
- ★☆☆☆☆
|
||||
f4:
|
||||
label: f4 label
|
||||
kind: Record
|
||||
options:
|
||||
labelField: f_label
|
||||
module: mod2
|
||||
queryFields:
|
||||
- f1
|
||||
|
||||
mod2:
|
||||
name: mod2 name
|
||||
fields:
|
||||
f_label:
|
||||
label: f_label record label
|
||||
f_ref_self:
|
||||
label: f_ref_self label
|
||||
kind: Record
|
||||
options:
|
||||
labelField: f_label
|
||||
module: mod2
|
||||
queryFields:
|
||||
- f1
|
||||
f1:
|
||||
label: f1 label
|
||||
kind: String
|
||||
required: true
|
||||
f2:
|
||||
label: f2 label
|
||||
kind: Record
|
||||
options:
|
||||
labelField: f_label
|
||||
module: mod3
|
||||
queryFields:
|
||||
- f1
|
||||
|
||||
mod3:
|
||||
name: mod3 name
|
||||
fields:
|
||||
f_label:
|
||||
label: f_label record label
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace: ns1
|
||||
pages:
|
||||
pg1:
|
||||
title: pg1 title
|
||||
blocks:
|
||||
- title: pg1 b1
|
||||
kind: Content
|
||||
xywh: [0, 0, 1, 1]
|
||||
options:
|
||||
body: pg1 b1 content body
|
||||
|
||||
- title: pg1 b2
|
||||
kind: RecordList
|
||||
xywh: [0, 1, 1, 1]
|
||||
options:
|
||||
module: mod1
|
||||
fields:
|
||||
- name: f1
|
||||
- name: f2
|
||||
variants:
|
||||
bodyBg: white
|
||||
border: primary
|
||||
headerBg: white
|
||||
headerText: primary
|
||||
|
||||
- title: pg1 b3
|
||||
kind: Chart
|
||||
xywh: [0, 2, 1, 1]
|
||||
options:
|
||||
chart: chr1
|
||||
variants:
|
||||
bodyBg: white
|
||||
border: primary
|
||||
headerBg: white
|
||||
headerText: primary
|
||||
|
||||
rpg2:
|
||||
handle: rpg2
|
||||
module: mod1
|
||||
title: Record page for module "mod1"
|
||||
blocks:
|
||||
- title: rpg2 b1 title
|
||||
kind: Record
|
||||
options:
|
||||
fields:
|
||||
- name: f1
|
||||
- name: f2
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace: ns1
|
||||
charts:
|
||||
chr1:
|
||||
name: chr1 name
|
||||
config:
|
||||
reports:
|
||||
- dimensions:
|
||||
- field: Status
|
||||
modifier: (no grouping / buckets)
|
||||
filter: ""
|
||||
metrics:
|
||||
- backgroundColor: '#11ff57'
|
||||
field: count
|
||||
fixTooltips: true
|
||||
type: pie
|
||||
module: mod1
|
||||
renderer: {}
|
||||
colorScheme: tableau.Tableau10
|
||||
Reference in New Issue
Block a user