mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
c405028fc5
Continuous Delivery / lint-and-build (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Property Based Tests / api-test (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
308 lines
8.6 KiB
Go
308 lines
8.6 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// preflight runs read-only data-quality checks before any write. Blocking issues
|
|
// (rep.block) abort an --apply run; warnings (rep.warn) are advisory. See PLAN §6.
|
|
func (m *migrator) preflight(ctx context.Context, rep *report) error {
|
|
checks := []func(context.Context, *report) error{
|
|
m.pfEmails,
|
|
m.pfHumanNames,
|
|
m.pfTenantNames,
|
|
m.pfTenantAlias,
|
|
m.pfEntityResourceAlias,
|
|
m.pfClientNames,
|
|
m.pfGroupNames,
|
|
m.pfOrphans,
|
|
}
|
|
for _, c := range checks {
|
|
if err := c(ctx, rep); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dupGroups returns the keys that appear more than once (with their count).
|
|
func dupGroups(keyOf func() []string) map[string]int {
|
|
seen := map[string]int{}
|
|
for _, k := range keyOf() {
|
|
if k != "" {
|
|
seen[k]++
|
|
}
|
|
}
|
|
for k, n := range seen {
|
|
if n < 2 {
|
|
delete(seen, k)
|
|
}
|
|
}
|
|
return seen
|
|
}
|
|
|
|
// pfEmails: Atom entity_emails.email is globally UNIQUE. Magistrala enforces this
|
|
// in the users table, but a dump merged across instances can break it.
|
|
func (m *migrator) pfEmails(ctx context.Context, rep *report) error {
|
|
users, err := readUsers(ctx, m.usersDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dups := dupGroups(func() []string {
|
|
out := make([]string, 0, len(users))
|
|
for _, u := range users {
|
|
if u.Email.Valid {
|
|
out = append(out, strings.ToLower(strings.TrimSpace(u.Email.String)))
|
|
}
|
|
}
|
|
return out
|
|
})
|
|
for email, n := range dups {
|
|
rep.blockf("email %q used by %d users (entity_emails.email is UNIQUE)", email, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfHumanNames: entities(name, tenant_id) is UNIQUE; humans have tenant_id NULL so
|
|
// they share one global name namespace. name = first of username/email/id.
|
|
func (m *migrator) pfHumanNames(ctx context.Context, rep *report) error {
|
|
users, err := readUsers(ctx, m.usersDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dups := dupGroups(func() []string {
|
|
out := make([]string, 0, len(users))
|
|
for _, u := range users {
|
|
out = append(out, firstNonEmpty(u.Username.String, u.Email.String, u.ID))
|
|
}
|
|
return out
|
|
})
|
|
for name, n := range dups {
|
|
// entities(name, tenant_id) is NULLS DISTINCT and humans have tenant_id
|
|
// NULL, so duplicate human names do NOT violate the index — advisory only.
|
|
rep.warnf("human entity name %q used by %d users (allowed; tenant NULL is NULLS DISTINCT)", name, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfTenantNames: tenants.name is UNIQUE and NOT NULL. Magistrala domain.name is
|
|
// nullable and non-unique, so empty or duplicate names break the load.
|
|
func (m *migrator) pfTenantNames(ctx context.Context, rep *report) error {
|
|
doms, err := readDomains(ctx, m.domainsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
names := make([]string, 0, len(doms))
|
|
for _, d := range doms {
|
|
if !d.Name.Valid || strings.TrimSpace(d.Name.String) == "" {
|
|
rep.warnf("domain %s has empty name -> will use its id (tenants.name is NOT NULL UNIQUE)", d.ID)
|
|
continue
|
|
}
|
|
names = append(names, d.Name.String)
|
|
}
|
|
for name, n := range dupGroups(func() []string { return names }) {
|
|
rep.warnf("domain name %q used by %d domains -> duplicates auto-renamed (tenants.name is UNIQUE)", name, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfGroupNames: object_groups(name, tenant_id) is UNIQUE. Magistrala dropped the
|
|
// groups (domain_id, name) constraint, so same-domain dups are possible.
|
|
func (m *migrator) pfGroupNames(ctx context.Context, rep *report) error {
|
|
grps, err := readGroups(ctx, m.groupsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keys := make([]string, 0, len(grps))
|
|
for _, g := range grps {
|
|
keys = append(keys, g.DomainID+"|"+g.Name)
|
|
}
|
|
for k, n := range dupGroups(func() []string { return keys }) {
|
|
rep.warnf("group name collision (%s) across %d groups in one tenant -> duplicates auto-renamed", k, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfTenantAlias: domain.route -> tenants.alias. Globally unique, case-folded,
|
|
// slug-shaped, not UUID-shaped. Invalid shape => alias dropped (warn). Case-fold
|
|
// collision among otherwise-valid aliases => block.
|
|
func (m *migrator) pfTenantAlias(ctx context.Context, rep *report) error {
|
|
doms, err := readDomains(ctx, m.domainsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
valid := []string{}
|
|
for _, d := range doms {
|
|
if !d.Route.Valid || d.Route.String == "" {
|
|
continue
|
|
}
|
|
a, ok := normalizeAlias(d.Route.String)
|
|
if !ok {
|
|
rep.warnf("tenant %s alias %q invalid slug -> dropped", d.ID, d.Route.String)
|
|
continue
|
|
}
|
|
valid = append(valid, a)
|
|
}
|
|
for a, n := range dupGroups(func() []string { return valid }) {
|
|
rep.warnf("tenant alias %q collides case-insensitively across %d domains -> duplicates auto-suffixed", a, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfEntityResourceAlias: client.identity / channel.route are unique per tenant
|
|
// (case-folded). Collision within a domain => block (would violate Atom's unique
|
|
// index mid-apply). Invalid shape => warn (dropped).
|
|
func (m *migrator) pfEntityResourceAlias(ctx context.Context, rep *report) error {
|
|
clients, err := readClients(ctx, m.clientsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chans, err := readChannels(ctx, m.channelsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// key = domain|alias
|
|
cKeys := []string{}
|
|
for _, c := range clients {
|
|
if !c.Identity.Valid || c.Identity.String == "" {
|
|
continue
|
|
}
|
|
a, ok := normalizeAlias(c.Identity.String)
|
|
if !ok {
|
|
rep.warnf("client %s alias %q invalid slug -> dropped", c.ID, c.Identity.String)
|
|
continue
|
|
}
|
|
cKeys = append(cKeys, c.DomainID+"|"+a)
|
|
}
|
|
for k, n := range dupGroups(func() []string { return cKeys }) {
|
|
rep.warnf("device alias collision (%s) across %d clients in one tenant -> duplicates auto-suffixed", k, n)
|
|
}
|
|
rKeys := []string{}
|
|
for _, ch := range chans {
|
|
if !ch.Route.Valid || ch.Route.String == "" {
|
|
continue
|
|
}
|
|
a, ok := normalizeAlias(ch.Route.String)
|
|
if !ok {
|
|
rep.warnf("channel %s alias %q invalid slug -> dropped", ch.ID, ch.Route.String)
|
|
continue
|
|
}
|
|
rKeys = append(rKeys, ch.DomainID+"|"+a)
|
|
}
|
|
for k, n := range dupGroups(func() []string { return rKeys }) {
|
|
rep.warnf("channel alias collision (%s) across %d channels in one tenant -> duplicates auto-suffixed", k, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfClientNames: device entities are unique on (name, tenant_id). Magistrala
|
|
// dropped the (domain_id, name) unique constraint, so same-domain name dups are
|
|
// possible and would break the Atom insert.
|
|
func (m *migrator) pfClientNames(ctx context.Context, rep *report) error {
|
|
clients, err := readClients(ctx, m.clientsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keys := []string{}
|
|
for _, c := range clients {
|
|
keys = append(keys, c.DomainID+"|"+firstNonEmpty(c.Name.String, c.ID))
|
|
}
|
|
for k, n := range dupGroups(func() []string { return keys }) {
|
|
rep.warnf("device name collision (%s) across %d clients in one tenant -> duplicates auto-renamed", k, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pfOrphans: clients/channels/groups whose domain_id has no surviving domain are
|
|
// skipped during load. Advisory only.
|
|
func (m *migrator) pfOrphans(ctx context.Context, rep *report) error {
|
|
doms, err := readDomains(ctx, m.domainsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
domSet := map[string]bool{}
|
|
for _, d := range doms {
|
|
domSet[d.ID] = true
|
|
}
|
|
count := func(get func() []string, label string) {
|
|
n := 0
|
|
for _, id := range get() {
|
|
if !domSet[id] {
|
|
n++
|
|
}
|
|
}
|
|
if n > 0 {
|
|
rep.warnf("%d %s reference a missing domain -> will be skipped", n, label)
|
|
}
|
|
}
|
|
clients, err := readClients(ctx, m.clientsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count(func() []string {
|
|
out := make([]string, len(clients))
|
|
for i, c := range clients {
|
|
out[i] = c.DomainID
|
|
}
|
|
return out
|
|
}, "clients")
|
|
chans, err := readChannels(ctx, m.channelsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count(func() []string {
|
|
out := make([]string, len(chans))
|
|
for i, c := range chans {
|
|
out[i] = c.DomainID
|
|
}
|
|
return out
|
|
}, "channels")
|
|
grps, err := readGroups(ctx, m.groupsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count(func() []string {
|
|
out := make([]string, len(grps))
|
|
for i, g := range grps {
|
|
out[i] = g.DomainID
|
|
}
|
|
return out
|
|
}, "groups")
|
|
rules, err := readRules(ctx, m.reDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count(func() []string {
|
|
out := make([]string, len(rules))
|
|
for i, r := range rules {
|
|
out[i] = r.DomainID
|
|
}
|
|
return out
|
|
}, "rules")
|
|
reports, err := readReports(ctx, m.reportsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count(func() []string {
|
|
out := make([]string, len(reports))
|
|
for i, r := range reports {
|
|
out[i] = r.DomainID
|
|
}
|
|
return out
|
|
}, "reports")
|
|
return nil
|
|
}
|
|
|
|
// preflightGate aborts an --apply run when blocking issues exist.
|
|
func (m *migrator) preflightGate(rep *report) error {
|
|
if m.apply && rep.HasBlocking() {
|
|
return fmt.Errorf("preflight found %d blocking issue(s); aborting apply (see report)", len(rep.Blocking))
|
|
}
|
|
return nil
|
|
}
|