We removed handle requirement for all resource like namespace(slug), module, user, page, chart. Due to that namespace import/export was broken as we were using handle as reference, hence fixed it by replacing handle usage with ID of resource. It also fixes index of template table for handle column.
805 lines
21 KiB
Go
805 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"mime/multipart"
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
|
|
automationService "github.com/cortezaproject/corteza/server/automation/service"
|
|
"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/auth"
|
|
"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"
|
|
"github.com/cortezaproject/corteza/server/pkg/label"
|
|
"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"
|
|
)
|
|
|
|
type (
|
|
namespace struct {
|
|
actionlog actionlog.Recorder
|
|
ac namespaceAccessController
|
|
modAc moduleAccessController
|
|
pageAc pageAccessController
|
|
chartAc chartAccessController
|
|
|
|
eventbus eventDispatcher
|
|
store store.Storer
|
|
locale ResourceTranslationsManagerService
|
|
}
|
|
|
|
namespaceImportSession struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"handle"`
|
|
NamespaceID uint64 `json:"namespaceID,string"`
|
|
SessionID uint64 `json:"sessionID,string"`
|
|
UserID uint64 `json:"userID,string"`
|
|
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
|
|
Resources resource.InterfaceSet `json:"-"`
|
|
}
|
|
|
|
namespaceAccessController interface {
|
|
CanManageResourceTranslations(ctx context.Context) bool
|
|
CanSearchNamespaces(context.Context) bool
|
|
CanCreateNamespace(context.Context) bool
|
|
CanReadNamespace(context.Context, *types.Namespace) bool
|
|
CanUpdateNamespace(context.Context, *types.Namespace) bool
|
|
CanDeleteNamespace(context.Context, *types.Namespace) bool
|
|
|
|
Grant(ctx context.Context, rr ...*rbac.Rule) error
|
|
}
|
|
|
|
NamespaceService interface {
|
|
FindByID(ctx context.Context, namespaceID uint64) (*types.Namespace, error)
|
|
FindByHandle(ctx context.Context, handle string) (*types.Namespace, error)
|
|
Find(context.Context, types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error)
|
|
FindByAny(context.Context, interface{}) (*types.Namespace, error)
|
|
|
|
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)
|
|
ImportInit(ctx context.Context, f multipart.File, size int64) (namespaceImportSession, error)
|
|
ImportRun(ctx context.Context, sessionID uint64, dup *types.Namespace, encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error)
|
|
DeleteByID(ctx context.Context, namespaceID uint64) error
|
|
}
|
|
|
|
namespaceUpdateHandler func(ctx context.Context, ns *types.Namespace) (namespaceChanges, error)
|
|
namespaceChanges uint8
|
|
)
|
|
|
|
const (
|
|
namespaceUnchanged namespaceChanges = 0
|
|
namespaceChanged namespaceChanges = 1
|
|
namespaceLabelsChanged namespaceChanges = 2
|
|
)
|
|
|
|
var (
|
|
// @todo this is a temporary implementation; we will rework resource import/export
|
|
// in the following versions
|
|
namespaceSessionStore = make(map[uint64]namespaceImportSession)
|
|
)
|
|
|
|
func Namespace() *namespace {
|
|
return &namespace{
|
|
ac: DefaultAccessControl,
|
|
modAc: DefaultAccessControl,
|
|
pageAc: DefaultAccessControl,
|
|
chartAc: DefaultAccessControl,
|
|
|
|
eventbus: eventbus.Service(),
|
|
actionlog: DefaultActionlog,
|
|
store: DefaultStore,
|
|
locale: DefaultResourceTranslation,
|
|
}
|
|
}
|
|
|
|
// search fn() orchestrates pages search, namespace preload and check
|
|
func (svc namespace) Find(ctx context.Context, filter types.NamespaceFilter) (set types.NamespaceSet, f types.NamespaceFilter, err error) {
|
|
var (
|
|
aProps = &namespaceActionProps{filter: &filter}
|
|
)
|
|
|
|
// For each fetched item, store backend will check if it is valid or not
|
|
filter.Check = func(res *types.Namespace) (bool, error) {
|
|
if !svc.ac.CanReadNamespace(ctx, res) {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
err = func() error {
|
|
if !svc.ac.CanSearchNamespaces(ctx) {
|
|
return NamespaceErrNotAllowedToSearch()
|
|
}
|
|
|
|
if len(filter.Labels) > 0 {
|
|
filter.LabeledIDs, err = label.Search(
|
|
ctx,
|
|
svc.store,
|
|
types.Namespace{}.LabelResourceKind(),
|
|
filter.Labels,
|
|
)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// labels specified but no labeled resources found
|
|
if len(filter.LabeledIDs) == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if set, f, err = store.SearchComposeNamespaces(ctx, svc.store, filter); err != nil {
|
|
return err
|
|
}
|
|
|
|
// i18n
|
|
tag := locale.GetAcceptLanguageFromContext(ctx)
|
|
set.Walk(func(n *types.Namespace) error {
|
|
n.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, n.ResourceTranslation()))
|
|
return nil
|
|
})
|
|
|
|
if err = label.Load(ctx, svc.store, toLabeledNamespaces(set)...); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}()
|
|
|
|
return set, f, svc.recordAction(ctx, aProps, NamespaceActionSearch, err)
|
|
}
|
|
|
|
func (svc namespace) FindByID(ctx context.Context, ID uint64) (ns *types.Namespace, err error) {
|
|
return svc.lookup(ctx, func(aProps *namespaceActionProps) (*types.Namespace, error) {
|
|
if ID == 0 {
|
|
return nil, NamespaceErrInvalidID()
|
|
}
|
|
|
|
aProps.namespace.ID = ID
|
|
return store.LookupComposeNamespaceByID(ctx, svc.store, ID)
|
|
})
|
|
}
|
|
|
|
// FindByHandle is an alias for FindBySlug
|
|
func (svc namespace) FindByHandle(ctx context.Context, handle string) (ns *types.Namespace, err error) {
|
|
return svc.FindBySlug(ctx, handle)
|
|
}
|
|
|
|
func (svc namespace) FindBySlug(ctx context.Context, slug string) (ns *types.Namespace, err error) {
|
|
return svc.lookup(ctx, func(aProps *namespaceActionProps) (*types.Namespace, error) {
|
|
if !handle.IsValid(slug) {
|
|
return nil, NamespaceErrInvalidHandle()
|
|
}
|
|
|
|
aProps.namespace.Slug = slug
|
|
return store.LookupComposeNamespaceBySlug(ctx, svc.store, slug)
|
|
})
|
|
}
|
|
|
|
// FindByAny tries to find namespace by id, handle or slug
|
|
func (svc namespace) FindByAny(ctx context.Context, identifier interface{}) (r *types.Namespace, err error) {
|
|
if ID, ok := identifier.(uint64); ok {
|
|
r, err = svc.FindByID(ctx, ID)
|
|
} else if strIdentifier, ok := identifier.(string); ok {
|
|
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
|
|
r, err = svc.FindByID(ctx, ID)
|
|
} else {
|
|
r, err = svc.FindByHandle(ctx, strIdentifier)
|
|
if err == nil && r.ID == 0 {
|
|
r, err = svc.FindBySlug(ctx, strIdentifier)
|
|
}
|
|
}
|
|
} else {
|
|
err = NamespaceErrInvalidID()
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Create adds namespace and presets access rules for role everyone
|
|
func (svc namespace) Create(ctx context.Context, new *types.Namespace) (*types.Namespace, error) {
|
|
var (
|
|
aProps = &namespaceActionProps{namespace: new}
|
|
)
|
|
|
|
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
|
if !handle.IsValid(new.Slug) {
|
|
return NamespaceErrInvalidHandle()
|
|
}
|
|
|
|
if !svc.ac.CanCreateNamespace(ctx) {
|
|
return NamespaceErrNotAllowedToCreate()
|
|
}
|
|
|
|
if err = svc.eventbus.WaitFor(ctx, event.NamespaceBeforeCreate(new, nil)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = svc.uniqueCheck(ctx, new); err != nil {
|
|
return err
|
|
}
|
|
|
|
new.ID = nextID()
|
|
new.CreatedAt = *now()
|
|
new.UpdatedAt = nil
|
|
new.DeletedAt = nil
|
|
|
|
aProps.setChanged(new)
|
|
|
|
if err = store.CreateComposeNamespace(ctx, svc.store, new); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = updateTranslations(ctx, svc.ac, svc.locale, new.EncodeTranslations()...); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = label.Create(ctx, s, new); err != nil {
|
|
return
|
|
}
|
|
|
|
_ = svc.eventbus.WaitFor(ctx, event.NamespaceAfterCreate(new, nil))
|
|
return nil
|
|
})
|
|
|
|
return new, svc.recordAction(ctx, aProps, NamespaceActionCreate, err)
|
|
}
|
|
|
|
func (svc namespace) Update(ctx context.Context, upd *types.Namespace) (c *types.Namespace, err error) {
|
|
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
|
|
if dup.Slug != "" {
|
|
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
|
|
}
|
|
|
|
aProps.setNamespace(dup)
|
|
_, err = svc.envoyRun(ctx, nn, targetNs, dup, encoder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dup, err = store.LookupComposeNamespaceBySlug(ctx, svc.store, dup.Slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tag := locale.GetAcceptLanguageFromContext(ctx)
|
|
dup.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, dup.ResourceTranslation()))
|
|
|
|
aProps.setNamespace(dup)
|
|
|
|
return nil
|
|
}()
|
|
|
|
return dup, svc.recordAction(ctx, aProps, NamespaceActionClone, err)
|
|
}
|
|
|
|
func (svc namespace) ImportInit(ctx context.Context, f multipart.File, size int64) (namespaceImportSession, error) {
|
|
var (
|
|
aProps = &namespaceActionProps{}
|
|
err error
|
|
ns *types.Namespace
|
|
session namespaceImportSession
|
|
)
|
|
|
|
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 {
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
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...)
|
|
}
|
|
|
|
// store a session for later
|
|
session = namespaceImportSession{
|
|
SessionID: nextID(),
|
|
UserID: auth.GetIdentityFromContext(ctx).Identity(),
|
|
|
|
CreatedAt: *now(),
|
|
Resources: nn,
|
|
}
|
|
|
|
// find the ns node
|
|
for _, n := range nn {
|
|
if nsn, ok := n.(*resource.ComposeNamespace); ok {
|
|
ns = nsn.Res
|
|
break
|
|
}
|
|
}
|
|
|
|
if ns == nil {
|
|
return NamespaceErrImportMissingNamespace()
|
|
}
|
|
|
|
// session needs to have namespaceID if ns Handle is not provided
|
|
session.NamespaceID = ns.ID
|
|
session.Name = ns.Name
|
|
session.Slug = ns.Slug
|
|
namespaceSessionStore[session.SessionID] = session
|
|
|
|
aProps.setNamespace(ns)
|
|
return nil
|
|
}()
|
|
|
|
return session, svc.recordAction(ctx, aProps, NamespaceActionImportInit, err)
|
|
}
|
|
|
|
func (svc namespace) ImportRun(ctx context.Context, sessionID uint64, dup *types.Namespace, encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error) {
|
|
var (
|
|
aProps = &namespaceActionProps{namespace: dup}
|
|
)
|
|
|
|
err = func() (err error) {
|
|
// access control
|
|
if err = svc.canImport(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !handle.IsValid(dup.Slug) {
|
|
return NamespaceErrInvalidHandle()
|
|
}
|
|
|
|
if dup.Slug != "" {
|
|
// check for duplicate
|
|
dstNs, err := store.LookupComposeNamespaceBySlug(ctx, svc.store, dup.Slug)
|
|
if err != nil && err != store.ErrNotFound {
|
|
return err
|
|
}
|
|
if dstNs != nil {
|
|
return NamespaceErrHandleNotUnique()
|
|
}
|
|
}
|
|
|
|
// session
|
|
var (
|
|
session namespaceImportSession
|
|
ok bool
|
|
newNS *types.Namespace
|
|
)
|
|
if session, ok = namespaceSessionStore[sessionID]; !ok {
|
|
return NamespaceErrImportSessionNotFound()
|
|
}
|
|
defer func() {
|
|
delete(namespaceSessionStore, sessionID)
|
|
}()
|
|
|
|
aProps.setNamespace(dup)
|
|
|
|
newNS, err = svc.envoyRun(ctx, session.Resources, &types.Namespace{ID: session.NamespaceID, Slug: session.Slug, Name: session.Name}, dup, encoder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
aProps.setNamespace(newNS)
|
|
return nil
|
|
}()
|
|
|
|
return dup, svc.recordAction(ctx, aProps, NamespaceActionImportRun, err)
|
|
}
|
|
|
|
func (svc namespace) DeleteByID(ctx context.Context, namespaceID uint64) error {
|
|
return trim1st(svc.updater(ctx, namespaceID, NamespaceActionDelete, svc.handleDelete))
|
|
}
|
|
|
|
func (svc namespace) UndeleteByID(ctx context.Context, namespaceID uint64) error {
|
|
return trim1st(svc.updater(ctx, namespaceID, NamespaceActionUndelete, svc.handleUndelete))
|
|
}
|
|
|
|
func (svc namespace) updater(ctx context.Context, namespaceID uint64, action func(...*namespaceActionProps) *namespaceAction, fn namespaceUpdateHandler) (*types.Namespace, error) {
|
|
var (
|
|
changes namespaceChanges
|
|
ns, old *types.Namespace
|
|
aProps = &namespaceActionProps{namespace: &types.Namespace{ID: namespaceID}}
|
|
err error
|
|
)
|
|
|
|
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
|
ns, err = loadNamespace(ctx, s, namespaceID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if err = label.Load(ctx, svc.store, ns); err != nil {
|
|
return err
|
|
}
|
|
|
|
old = ns.Clone()
|
|
|
|
aProps.setNamespace(ns)
|
|
aProps.setChanged(ns)
|
|
|
|
if ns.DeletedAt == nil {
|
|
err = svc.eventbus.WaitFor(ctx, event.NamespaceBeforeUpdate(ns, old))
|
|
} else {
|
|
err = svc.eventbus.WaitFor(ctx, event.NamespaceBeforeDelete(ns, old))
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if changes, err = fn(ctx, ns); err != nil {
|
|
return err
|
|
}
|
|
|
|
if changes&namespaceChanged > 0 {
|
|
if err = store.UpdateComposeNamespace(ctx, svc.store, ns); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = updateTranslations(ctx, svc.ac, svc.locale, ns.EncodeTranslations()...); err != nil {
|
|
return
|
|
}
|
|
|
|
if changes&namespaceLabelsChanged > 0 {
|
|
if err = label.Update(ctx, s, ns); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if ns.DeletedAt == nil {
|
|
err = svc.eventbus.WaitFor(ctx, event.NamespaceAfterUpdate(ns, old))
|
|
} else {
|
|
err = svc.eventbus.WaitFor(ctx, event.NamespaceAfterDelete(nil, old))
|
|
}
|
|
|
|
return err
|
|
})
|
|
|
|
return ns, svc.recordAction(ctx, aProps, action, err)
|
|
}
|
|
|
|
// lookup fn() orchestrates namespace lookup, and check
|
|
func (svc namespace) lookup(ctx context.Context, lookup func(*namespaceActionProps) (*types.Namespace, error)) (ns *types.Namespace, err error) {
|
|
var aProps = &namespaceActionProps{namespace: &types.Namespace{}}
|
|
|
|
err = func() error {
|
|
if ns, err = lookup(aProps); errors.IsNotFound(err) {
|
|
return NamespaceErrNotFound()
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
aProps.setNamespace(ns)
|
|
|
|
if !svc.ac.CanReadNamespace(ctx, ns) {
|
|
return NamespaceErrNotAllowedToRead()
|
|
}
|
|
|
|
if err = label.Load(ctx, svc.store, ns); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}()
|
|
|
|
return ns, svc.recordAction(ctx, aProps, NamespaceActionLookup, err)
|
|
}
|
|
|
|
func (svc namespace) uniqueCheck(ctx context.Context, ns *types.Namespace) (err error) {
|
|
if ns.Slug != "" {
|
|
if e, _ := store.LookupComposeNamespaceBySlug(ctx, svc.store, ns.Slug); e != nil && e.ID != ns.ID {
|
|
return NamespaceErrHandleNotUnique()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc namespace) handleUpdate(ctx context.Context, upd *types.Namespace) namespaceUpdateHandler {
|
|
return func(ctx context.Context, res *types.Namespace) (changes namespaceChanges, err error) {
|
|
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
|
|
return namespaceUnchanged, NamespaceErrStaleData()
|
|
}
|
|
|
|
if upd.Slug != res.Slug && !handle.IsValid(upd.Slug) {
|
|
return namespaceUnchanged, NamespaceErrInvalidHandle()
|
|
}
|
|
|
|
if err := svc.uniqueCheck(ctx, upd); err != nil {
|
|
return namespaceUnchanged, err
|
|
}
|
|
|
|
if !svc.ac.CanUpdateNamespace(ctx, res) {
|
|
return namespaceUnchanged, NamespaceErrNotAllowedToUpdate()
|
|
}
|
|
|
|
if res.Name != upd.Name {
|
|
changes |= namespaceChanged
|
|
res.Name = upd.Name
|
|
}
|
|
|
|
if res.Slug != upd.Slug {
|
|
changes |= namespaceChanged
|
|
res.Slug = upd.Slug
|
|
}
|
|
|
|
if res.Enabled != upd.Enabled {
|
|
changes |= namespaceChanged
|
|
res.Enabled = upd.Enabled
|
|
}
|
|
|
|
if !reflect.DeepEqual(upd.Meta, res.Meta) {
|
|
changes |= namespaceChanged
|
|
res.Meta = upd.Meta
|
|
}
|
|
|
|
if upd.Labels != nil {
|
|
if label.Changed(res.Labels, upd.Labels) {
|
|
changes |= namespaceLabelsChanged
|
|
res.Labels = upd.Labels
|
|
}
|
|
}
|
|
|
|
if changes&namespaceChanged > 0 {
|
|
res.UpdatedAt = now()
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
func (svc namespace) handleDelete(ctx context.Context, ns *types.Namespace) (namespaceChanges, error) {
|
|
if !svc.ac.CanDeleteNamespace(ctx, ns) {
|
|
return namespaceUnchanged, NamespaceErrNotAllowedToDelete()
|
|
}
|
|
|
|
if ns.DeletedAt != nil {
|
|
// namespace already deleted
|
|
return namespaceUnchanged, nil
|
|
}
|
|
|
|
ns.DeletedAt = now()
|
|
return namespaceChanged, nil
|
|
}
|
|
|
|
func (svc namespace) handleUndelete(ctx context.Context, ns *types.Namespace) (namespaceChanges, error) {
|
|
if !svc.ac.CanDeleteNamespace(ctx, ns) {
|
|
return namespaceUnchanged, NamespaceErrNotAllowedToUndelete()
|
|
}
|
|
|
|
if ns.DeletedAt == nil {
|
|
// namespace not deleted
|
|
return namespaceUnchanged, nil
|
|
}
|
|
|
|
ns.DeletedAt = nil
|
|
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 (svc namespace) envoyRun(ctx context.Context, resources resource.InterfaceSet, oldNS, newNS *types.Namespace, encoder func(resource.InterfaceSet) error) (ns *types.Namespace, err error) {
|
|
// Handle renames and references
|
|
oldNsRef := resource.MakeRef(types.NamespaceResourceType, resource.MakeIdentifiers(oldNS.Slug, oldNS.Name, strconv.FormatUint(oldNS.ID, 10)))
|
|
newNsRef := resource.MakeRef(types.NamespaceResourceType, resource.MakeIdentifiers(newNS.Slug, newNS.Name))
|
|
|
|
auxNs := resource.FindComposeNamespace(resources, oldNsRef.Identifiers)
|
|
auxNs.ID = 0
|
|
auxNs.Name = newNS.Name
|
|
auxNs.Slug = newNS.Slug
|
|
newNS = auxNs
|
|
ns = newNS
|
|
|
|
// Correct internal references
|
|
// - namespace identifiers
|
|
resources.SearchForIdentifiers(oldNsRef.ResourceType, oldNsRef.Identifiers).Walk(func(r resource.Interface) error {
|
|
r.ReID(newNsRef.Identifiers)
|
|
return nil
|
|
})
|
|
// - relations
|
|
resources.SearchForReferences(oldNsRef).Walk(func(r resource.Interface) error {
|
|
r.ReRef(resource.RefSet{oldNsRef}, resource.RefSet{newNsRef})
|
|
return nil
|
|
})
|
|
|
|
// run the import
|
|
err = encoder(resources)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Adjust name res. tr. since we're changing it
|
|
if err = updateTranslations(ctx, svc.ac, svc.locale, &locale.ResourceTranslation{
|
|
Resource: auxNs.ResourceTranslation(),
|
|
Key: types.LocaleKeyNamespaceName.Path,
|
|
Msg: locale.SanitizeMessage(auxNs.Name),
|
|
}); err != nil {
|
|
return
|
|
}
|
|
|
|
// Reload RBAC rules (in case import brought in something new)
|
|
rbac.Global().Reload(ctx)
|
|
if err = locale.Global().ReloadResourceTranslations(ctx); err != nil {
|
|
return
|
|
}
|
|
|
|
// Reload workflow-triggers (in case import brought in something new)
|
|
if err = automationService.DefaultWorkflow.Load(ctx); err != nil {
|
|
// should not be a fatal error
|
|
err = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func loadNamespace(ctx context.Context, s store.ComposeNamespaces, namespaceID uint64) (ns *types.Namespace, err error) {
|
|
if namespaceID == 0 {
|
|
return nil, ChartErrInvalidNamespaceID()
|
|
}
|
|
|
|
if ns, err = store.LookupComposeNamespaceByID(ctx, s, namespaceID); errors.IsNotFound(err) {
|
|
return nil, NamespaceErrNotFound()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// toLabeledNamespaces converts to []label.LabeledResource
|
|
//
|
|
// This function is auto-generated.
|
|
func toLabeledNamespaces(set []*types.Namespace) []label.LabeledResource {
|
|
if len(set) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ll := make([]label.LabeledResource, len(set))
|
|
for i := range set {
|
|
ll[i] = set[i]
|
|
}
|
|
|
|
return ll
|
|
}
|