Only ErrQueueClosed was examined and every other error was discarded, so a
run where nothing reached a queue at all would leave the refusal count at
zero and the test green. The first unexpected error is now kept and fails
the test.
The pool is sized so a full queue cannot be one of them: it returns an error
of its own and is expected while the consumer is held, which would otherwise
make the new check fire on the normal path.
Two fixed sleeps decided when this test looked: one to let the consumer pick
a message up, one to let publishes accumulate inside the reload. Both are
guesses about how fast the runner is. The consumer now signals on its first
delivery, and the measurement waits until enough publishes have landed.
The "nothing was published" guard goes with them. It was the weaker form of
the same check, and it ran after the fact instead of holding the window open
until there was something to measure.
The assertion demanded zero refusals during a reload, and this ordering
cannot deliver that. GetQueuePrefix returns a wrapper that captured the
adapter, so a producer that fetched before the swap and appends after
Shutdown has begun is still holding the old queue. That window is one call
wide; closing it means resolving the adapter inside Append, which is core's
to change.
What the ordering removes is the sustained window - every producer that
fetches during the wait. Measured on a race-enabled run: 174 of 174 publishes
refused with the old order, 1 of 174 with the new one. The old assertion
therefore failed about half the time on a change that works.
The publishing goroutine was told to stop and never waited for. t.Cleanup
restores sdk.Runtime while a producer that has not yet noticed the stop is
still reading it, which -race reports as a write and a read on the same
package variable. Signalling is not joining.
Shutdown now waits for its consumers to deliver what the queue still holds, and
setupQueue called it first. For that whole wait sdk.Runtime still pointed at the
adapter that had stopped accepting, so every Append landing in the window came
back ErrQueueClosed - and both call sites in common/middleware log that at error
level while the row never reaches the database.
Measured with a held consumer: 177 of 178 publishes during one reload.
Installing first leaves no window. A producer fetches the adapter per call and
gets either the new queue or the old one, and both accept; the old one still
drains, because Shutdown is what waits for that.
The difference only exists during the wait - after Setup returns the two orders
look identical, which is why the test holds a consumer and publishes throughout
the reload rather than checking the state afterwards.
The counter-proof compiles and reports the 177.
One fixed key meant two probes overlapping - two /ready requests, or two
instances against the one redis, which is the normal deployment - overwrote each
other's value between the write and the read, and each concluded the cache was
broken. A readiness probe that reports false negatives under load pulls healthy
instances out of the pool, which is worse than not probing.
The key now carries eight random bytes. The written value is still read back,
because that is what tells a healthy cache apart from one that answers "miss"
for everything, and the key is deleted afterwards on a best-effort basis - its
error is dropped deliberately, since the verdict is already decided and a cache
that cannot delete what it just wrote is not a reason to refuse traffic.
The first version of the concurrency test had no teeth: the probes are short
enough that the scheduler ran them one after another, so the fixed-key
counter-proof passed three times out of three. The fake cache now holds every
writer until all of them have written, which makes the interleaving the test is
about actually happen. With one key that is a deterministic 15 failures out of
16 - only the last writer's value survives - and with a key per probe, none.
/health returned 200 without asking anything. Whatever it was meant to say, an
orchestrator reading it learned only that a process was accepting connections.
The two questions are not the same one, and the answers differ:
- /health stays a bare 200. It answers "should I restart you", and a process
whose database is unreachable does not want restarting - that turns one
outage into a crash loop and discards the connection pool, the cache and
every request in flight on the way.
- /ready is new. It answers "should I send you requests", fails while a
dependency is unreachable, and fails from the moment shutdown begins.
That last part is what the life-cycle phases bought. BeginDraining sits next to
BeginShutdown, before the server stops accepting, so a load balancer is told to
stop sending while this instance can still finish what it holds. Reversed - and
that is where it was - the connections are cut first and the probe reports it
afterwards.
The queue is deliberately not checked. Nothing on AdapterQueue answers "are you
reachable" without publishing something, the memory backend cannot fail, and a
queue that is down degrades logging rather than stopping requests: a reason to
alert, not a reason to leave the pool.
The cache probe writes and reads back rather than only reading. A cache that
answers "miss" for every key - a client pointed at the wrong server - is
indistinguishable from a healthy one on a read alone.
Every check runs behind a recover, and that is not defensive habit. The test for
"nothing configured" found the reason: GetCacheAdapter builds a wrapper around
whatever is configured and returns it even when nothing is, so the value is not
nil, the cache inside it is, and Set dereferences it. A nil check cannot see
that, and GetQueueAdapter behaves the same way. Whatever the cause, a probe is
the last thing that should be able to take the process down - the caller is
asking whether this instance is well, and killing it is the wrong reply.
The counter-proof compiles and fails: without the recover, the unconfigured case
panics rather than reporting two failed checks.
Setup installs the cache and queue adapters on sdk.Runtime, which is a
package-level singleton, and records what it installed in this package's own
variables. The test left all of it behind, so what a later test in this binary
saw depended on whether this one had run - the leak cmd/api's freshRuntime and
common/middleware's copy of it exist to prevent.
sdk.Runtime is swapped for a fresh one and everything is put back in Cleanup.
Issue #892 is that a reload replaces the queue adapter and the consumers
registered against the previous one are left attached to a queue nobody
publishes to any more.
The fix has two halves and only one of them was covered. attachQueueConsumers
gives a new queue its own consumers and the same queue none, which cmd/api
tests against a queue it controls by passing generation numbers in by hand. What
nothing asserted is that a reload actually produces a new generation for it to
notice - the half that lives in this package.
Setup is what config re-runs on every change, so calling it twice is what a
reload does here. The generation goes 0, 1, 2.
The counter-proof compiles and fails: dropping the increment in setupQueue
reports that the first Setup installed no queue at all, because a generation
that never moves is indistinguishable from never having been set.
This closes the loop rather than adding coverage for its own sake: with both
halves asserted, the claim that #892 is fixed rests on tests instead of on
reading the two functions and believing they meet.
AfterListen promises a hook that the port answers. The bind was moved onto the
caller's goroutine to keep that promise, but with ssl enabled there was a second
way to fail after the announcement: ServeTLS reads the certificate files itself,
on the serving goroutine, so a bad path or an unreadable key surfaced once the
hooks had already run.
tls.LoadX509KeyPair now runs before anything is announced, and its error is
returned from startServing the way a failed bind is. ServeTLS still does the
real work - handing it a tls.Listener built here instead would take over the
HTTP/2 negotiation it sets up, and quietly drop h2 for every TLS deployment. The
cost is one extra read of the certificate at startup.
The fatal in the serving goroutine said "listen:". Neither the bind nor the
certificate reaches it any more, so it says "serve:".
The test covers the certificate path alongside the bind: neither may announce
the phase, and neither may seal it.
The counter-proof is not clean, and saying so is the point. Removing the check
does turn the run red, but through log.Fatal killing the process from the
serving goroutine - "fatal serve: open no-such.pem: no such file or directory" -
rather than through the assertion. That still demonstrates the defect, because
the process could only get there after startServing had returned successfully
and the phase had been announced; it cannot be observed from inside the test,
because the fatal races the assertion that would report it.
Also: the redis-backed queue tests now fail instead of skipping when CI is set
and GO_ADMIN_TEST_REDIS_ADDR is not. A workflow that renamed the variable or
dropped the service would otherwise stay green while those two tests quietly did
nothing - the same shape as the defect they exist to cover. Locally, with no CI
in the environment, they still skip.
The rule that consumers are registered before the queue is started had no test
that could fail on the backend it exists for. Everything so far ran on the
memory queue, which is the default: queue.Memory's Register starts another
consumer goroutine whatever the state, so the wrong order passes there. A suite
that only exercises the default reports success for a queue that accepts no
consumers at all.
CI gets a redis service, and two tests build the queue the way setupQueue does -
through config.QueueConfig.Setup, so what is under test is the adapter this
repository actually gets, LegacyQueueAdapter included. They skip without
GO_ADMIN_TEST_REDIS_ADDR, so a developer with no server still gets a green run.
Registering first and starting second delivers the message. Starting first and
registering second is refused: no consumer group was created, so Append comes
back with storage.ErrNoHandler. Pinning that particular error rather than "some
error" is deliberate - the test is about the missing consumer, and a connection
failure that happened to error too would otherwise pass for it.
Running it corrected something written two commits ago. The claim there was
that a late registration loses consumers "with nothing said". Only half of that
holds: the registration is silent, because Register returns nothing, but every
publish afterwards fails loudly - ErrNoHandler, logged at error level by both
call sites in common/middleware - while the log rows are never written. The
symptom is missing rows plus a lot of noise, not a quiet nothing. That commit's
message and the contract doc both say so now.
setupQueue ended with `go queueAdapter.Run()`, and the three log consumers were
registered afterwards, from setup(). The contract implementations refuse a
registration once the queue is running - memqueue and the redis queue both
answer storage.ErrQueueAlreadyStarted - and Register cannot report it: it
returns nothing, which its own comment in core records as the reason the
interface is deprecated. Start first and register second, across two
goroutines, and the registration is dropped without the caller being able to
tell.
What follows is not quiet. No consumer group was created, so redis refuses
every later publish with storage.ErrNoHandler, and go-admin logs that at error
level from both call sites while the login and operation log rows are simply
never written. The silence is in the registration; the cost shows up on every
request after it.
Which implementation is behind the interface depends on the configuration.
config.QueueConfig.Setup returns queue.NewMemory directly when there is no
redis section - and that one does not care about the order, because its
Register just starts another consumer goroutine. Only a redis section reaches
storage.LegacyQueueAdapter, which wraps the contract implementation and
therefore refuses. So the defect is invisible in the default deployment and
shows up only where redis is configured, dropping the login log, the operation
log and the api check - the three things #892 was about.
The start therefore moves to the code that registers, and nothing starts the
queue but that.
The registration also moves onto AfterResource. It has to: a reload rebuilds
the adapter, and consumers attached to the one that existed at start-up are
attached to a queue nobody publishes to any more. Being on that phase means
running again on every reload, so the callback is idempotent with respect to a
given queue rather than "does nothing the second time" - registering twice on
the same queue would give every message two consumers and write every log row
twice.
Identity for that comes from common/storage, where the adapter is built, as a
generation counter. It cannot come from the accessors: GetQueueAdapter and
GetQueuePrefix build a fresh runtime.Queue wrapper on every call, so comparing
two of them compares two wrappers and never matches however many times the
adapter underneath has been replaced. A counter also keeps the comparison on a
uint64 rather than an `==` between two interface values, which would panic on
an adapter type that is not comparable.
Generation 0 means the configuration has no queue section, so nothing was
installed and the runtime hands back its own memory queue. That case still gets
consumers, because the registration this replaces was unconditional and
dropping it would stop the logs for anyone who commented the section out.
Two things fixed on the way past:
- `if q := sdk.Runtime.GetQueueAdapter(); q != nil { q.Shutdown() }` was
always true. GetQueueAdapter never returns nil - with nothing configured
the runtime falls back to its own memory queue and wraps that - so the
first start shut down the fallback queue before anything had used it. Only
an adapter this package installed is shut down now.
- config.Setup becomes bootstrap.SetupConfig, which is what announces
AfterResource, and announces it after the callbacks that build the
resources rather than before.
attachConsumersOnce is split out so the order and the once-ness can be checked
against a queue the test controls; neither can be read back out of a real
adapter. Four tests cover the ordering, both directions of the idempotency
rule, and the unconfigured case. Both counter-proofs compile and fail: calling
Run before the registrations reports each of the three as "came after Run", and
dropping the generation guard reports eight calls where four are wanted.
One honest limit: the counter-proof for the ordering makes Run synchronous.
The original arrangement started the queue on another goroutine, and a race
cannot be made to fail every time - which is the reason the order is enforced
by structure here instead of being left to be noticed in use.
Two properties the previous shape broke silently: GetHandlerFunc must
report ok for the JwtToken key, and every module must read back the same
instance. Reverting the registration to the unbound method expression
still compiles and turns the first of these red, which is the failure
this pins.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
Four modules each called AuthInit and built their own instance, so which
one Runtime handed back was decided by whichever module initialised last.
The JwtToken key was also registered as an unbound method expression,
which GetHandlerFunc's type assertion can never match - the key was
registered and unusable at the same time.
The instance is now built once here and registered as a bound closure.
Modules read it back through GetAuthMiddleware, which is fatal rather
than nil when called before InitMiddleware has run: a process without a
JWT middleware should not reach the point of serving a request.
Only one call site needs the instance itself rather than the handler
(admin's /login, for LoginHandler); the thirty-odd MiddlewareFunc() call
sites are unchanged.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
IndexAction closes over one dto.Index and serves every request to the route
from it; Generate exists so each request gets its own instance, and every
implementation in this repository returns a copy for that reason. The probe
returned the receiver, which made it the one shape IndexAction is not
written against - and inconsistent with probeRow in the same file, which
already copied.
A single-request test cannot tell the two apart, so the assertion is on
Generate itself rather than on the action's behaviour.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
create.go/delete.go/index.go/update.go/view.go were not lowered to
core (PRD 006 F3) and still call actions.Permission directly, in this
repository, on a code path core's own test suite knows nothing about:
core pins down what Permission builds for a given scope, but nothing
covered whether this package's five Actions still remember to call it
at all. TestIndexActionAppliesDataPermission runs IndexAction exactly
as a real request would, against a real in-memory database, and
inspects the SQL GORM actually executed - not just that the handler
returned success, which it would just as happily do with the filter
missing entirely.
The SQL is captured through a gorm.io/gorm/logger.Interface wrapper
rather than read back from IndexAction's own *gorm.DB: IndexAction
builds and executes its query in one unbroken chain
(Model().Scopes().Find()...Count()) and never hands the built
statement back to its caller, so there is nothing else to inspect it
through.
Counterproof performed and reverted (not part of this commit): with
Permission(object.TableName(), p) removed from IndexAction's Scopes
call, the test failed with the captured SQL carrying no WHERE clause
at all (`SELECT * FROM action_probe_row LIMIT 10`); index.go was then
restored to its committed content (`git diff --exit-code` verified
clean).
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
DataPermission, PermissionAction, Permission, GetPermissionFromContext,
IsValidDataScope, PermissionKey and the five DataScope* constants now
forward to go-admin-core's sdk/contract/actions, which carries the
already-fixed logic from feat/006-security-prereq (PRD 006 F14/H1-H3).
create.go/delete.go/index.go/update.go/view.go - the five generic CRUD
actions - are untouched: they call Permission and
GetPermissionFromContext by the same names, which now resolve to
forwards with identical behaviour, and stay in this package rather
than moving to core (PRD 006 F3: core's exports are a permanent
promise every fork inherits, and CRUD shape is this framework's most
volatile surface).
PermissionKey is declared as `const PermissionKey =
contractactions.PermissionKey`, a direct reference rather than a
restated literal, per PRD 006's hard constraint 4: PermissionAction
sets this gin context key and GetPermissionFromContext reads it back,
and an independently declared copy could silently drift from core's if
one were ever edited without the other. permission_test.go replaces
the detailed data-permission regression suite - which now lives in
core, next to the logic itself - with a test of this package's own
wiring: that PermissionAction and both of this package's own read
paths (GetPermissionFromContext, and c.Get(actions.PermissionKey)
directly) still meet on the same key.
Counterproof performed and reverted (not part of this commit): with
PermissionKey redeclared here as the literal "dataPermission" and
core's copy changed to a different value, TestPermissionKeyMatches-
WhatPermissionActionSets went red while GetPermissionFromContext's own
round-trip stayed green - confirming the exported constant, not the
GetPermissionFromContext wrapper, is what an independent literal would
put at risk.
PRD 006 F3/F5.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
AutoForm, ObjectById/ObjectGetReq/ObjectDeleteReq, Pagination,
GeneralDelDto/GeneralGetDto and Index/Control are now type aliases of
go-admin-core's sdk/contract/dto; OrderDest, MakeCondition and
Paginate forward to the same package (functions cannot be aliased the
way types can).
MakeCondition no longer reads common/global.Driver to choose which SQL
dialect to resolve search tags against. The lowered version reads
db.Dialector.Name() from inside the closure it returns instead, which
is always the driver the caller's own *gorm.DB is bound to - correct
even with more than one database open with different drivers, which a
single process-wide variable could never be. global.Driver is marked
Deprecated accordingly; it is still set and still readable for fork
code that reads it directly.
PRD 006 F2/F5.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
ControlBy, Model, ModelTime, ActiveRecord, BaseUser, Response, Page,
Migration and the menu type constants now read `type X = pkg.X` /
`const X = pkg.X` against go-admin-core's sdk/contract/models instead
of defining these shapes locally. Every embed, GORM tag and JSON tag
is unchanged - a type alias is the same type, not a new one - and
every existing import of go-admin/common/models keeps compiling with
no changes of its own (verified with `git diff --exit-code` over the
70 files that import common/models, common/dto or common/actions).
The menu type constants (Directory/Menu/Button) are declared as direct
references rather than restated literals: an independently written
copy of the same value can be edited out of step with go-admin-core's,
where a direct reference cannot (PRD 006 hard constraint 4).
PRD 006 F1/F5.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
A table over all five scopes plus the ones that are not scopes, asserting the
generated SQL rather than a boolean, because the defect was that two different
intentions produced the same query.
The rows that matter are the negative ones: an unrecognized value, a zero
value, and a department scope with a non-positive id. Each was verified to go
red with its own fix reverted and the others in place, so a regression names
the defect it belongs to.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
dept_path is built as "/0/" + id + "/..." for every department, so a DeptId of
0 turns the department-tree pattern into '%/0/%' - which matches every row in
sys_dept. The scope meant to narrow visibility to one subtree returned the
whole organisation instead.
Both department scopes now refuse a non-positive id rather than building a
pattern from it. The admin DTO validates deptId, but seed scripts, SSO and
third-party registration paths do not, and after the contract move the caller
is no longer ours to control.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
The switch ended in `default: return db`, which is the same answer as "this
role may see everything". That made a legitimate scope indistinguishable from a
broken one: "1" (all data) had no case of its own and fell into default too, so
"1", "", "6" and a zero value all produced byte-identical SQL.
Three changes, in this order, because reversing them would break "1":
- the five scope values become named constants, so a reader can tell which
string means what without consulting the seed data
- "1" gets an explicit case, which is what frees default to mean "not a
scope I recognise"
- default now matches nothing rather than everything
SysRole DTOs accept the scope unvalidated, so an empty string reaches this
switch from ordinary use, not just from a corrupted row.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
PermissionAction logged the error and returned. Gin treats a plain return as
"carry on", so the request reached the business handler with PermissionKey
never set - and a zero DataPermission means Permission() adds no WHERE clause
at all. A database hiccup turned into full visibility, silently.
The neighbouring newDataPermission branch already aborts. This one now does the
same.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
PUT /api/v1/sys-user sits in CasbinExclude so the profile page can reach it,
which means AuthCheckRole never runs for this route. The handler took the
target user id from the request body, so any authenticated caller could edit
another user's record - including their roleId.
The route has to stay excluded: the profile page and the admin user list share
this one endpoint, so removing the exclusion would break self-service editing
for every non-admin role. The check therefore moves into the handler: when the
target is not the caller, the request is put through Casbin explicitly.
EnforceRoleFor carries the same admin short-circuit and enforcement AuthCheckRole
uses, so a route that opts out of the middleware can still ask the same question.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
The warning on Authorizator matters to anyone keeping a copy of that file, not
to one particular consumer, and it reads better addressed to all of them: check
what reads those context keys before taking this change.
The map Authorizator receives is built by IdentityHandler in the same file and
carries IdentityKey / UserName / RoleKey / UserId / RoleIds / DataScope - not
user and role. Both assertions failed on every request, and because the ok
result was discarded the five c.Set calls stored zero values and the function
returned true anyway. Nothing in this repository or in core reads those keys.
go-admin-pro has its own copy of this file and does read them; this change
must not be carried over there verbatim.
common/middleware imported app/admin/service/dto for two string constants,
which made a package apps are told to build on depend on one particular app.
LoggerToFile is registered on the engine, so every POST, PUT, GET and
DELETE had its body copied into memory - through a bytes.Buffer, a
ReadAll and a string conversion - before any handler ran. The only
consumer is operParam on the operation-log row, which is written when
logger.enableddb is on, and that is off in the shipped configuration.
There was no size limit either, and a file upload is a POST like any
other: a 1MB request allocated 4.3MB here and a 16MB upload allocated
about 67MB, to build a value nobody stored.
The body is now read only when the operation log will use it, and at
most 32KB of it. The handler still receives the whole request: it reads
the copied part from memory and the rest from the connection, so what
this holds is bounded however large the request is. 32KB also keeps the
value inside the TEXT column it is written to.
The bufio.Writer this replaces was never flushed. Nothing was truncated
only because bytes.Buffer implements io.ReaderFrom, so io.Copy bypassed
the buffer entirely - a different destination would have dropped the
tail of every request body.
AuthCheckRole walks CasbinExclude for every non-admin request, and used
casbin's util.KeyMatch2 to test each entry. That delegates to
util.RegexMatch, which is regexp.MatchString - it compiles its pattern on
every call - so a 32-entry list cost about 2,566 allocations per request
before the request reached Enforce.
Test the method first, which rules out most entries with a string
compare, and take the path test from go-admin-core, whose KeyMatch2
answers the same thing without recompiling. The scan drops to 52ns and no
allocations.
The loop moves out of AuthCheckRole so the tests exercise the code a
request runs rather than a copy of it, and an allocation budget fails if
the uncached matcher comes back.
setupSimpleDatabase runs once per configured database - one per host in
the multi-tenant configuration - and passed the same empty key to
mycasbin.Setup every time. Setup caches per key, so every host after the
first was handed the enforcer built from the first host's database and
was authorized against a casbin_rule table that was not its own.
Takes effect with the go-admin-core release that keys the cache; before
it, Setup ignored the argument entirely.
A rejected request answered 200 with the failure only in the body, so every
layer that reads the status line counted it as served: load balancers, metrics,
client-side retry. A load test against this reported the limiter's own
rejections as successful traffic and overstated throughput more than tenfold.
The threshold was a constant in the middleware, which made 200 QPS the ceiling
of every deployment with nothing in the configuration to reveal it. It now
reads extend.rateLimit.inboundQPS; an absent value keeps 200, so an upgrade
changes nothing, and zero disables the limiter for a deployment behind its own
gateway.
Also drops Strategy: system.BBR. Reading sentinel's source, the adaptive
strategy is consulted only for Load and CpuUsage - for InboundQPS the trigger
count is compared directly - so it read as if the limit adapted to the machine
when it never did.
The startup line printed the DSN whole:
* => goadmin:<password>@tcp(host:3306)/go-admin?...
So every deployment wrote its own database credential into its own logs,
where a log shipper, a support bundle or a screenshot of a terminal
carries it onward. Found while reading deploy output, which is exactly
how it leaks.
The host and username stay - they are what makes the line worth printing
- and only the password is replaced. Both DSN shapes this project accepts
are covered, a sqlite path is left alone, and anything unparseable is
withheld rather than echoed, since it may hold a credential too.
The scope is decided by the user id, the role id, the department and the
data_scope string. Three of the four were already in the token; deptid
was not, though core's user.GetDeptId has always read that claim. Adding
it removes a sys_user join from every list, detail, update and delete.
This goes no more stale than rolekey does, which Casbin has read from
the token since the beginning: both settle on the next login.
A token minted before this still works. Its claims are incomplete, and
the lookup runs for it as before.
Permission() returns the query untouched when EnableDP is false, so the
lookup feeding it has nothing to feed. The lookup ran anyway: a sys_user
join against sys_role on every list, detail, update and delete, with the
result discarded.
enabledp is false in settings.full.yml, so this was the default.
Each implementation keeps its provider client in an interface{} field that
Setup assigns, so an unconfigured store holds nil - and asserting nil to
the provider's client type panics:
panic: interface conversion: interface {} is nil, not *oss.Client
The upload endpoint reaches that path for any request naming a provider
the deployment never configured.
Three more things were wrong in the same files. OXS.Setup printed a
failure and returned the store anyway, handing back exactly the broken
object that panics. HuaWeiOBS.UpLoad printed the provider's error and
returned nil, so a failed upload reported success. Both it and
QiNiuKODO.UpLoad asserted the local path was a string without checking.
The tests asked the reader to paste their own credentials, so they failed
for everyone who did not. They now cover the guards and skip the part
that needs a provider unless credentials are in the environment.
Every import of the module changes, not only the seven packages that
moved out of sdk/pkg: Go requires the major version in the path from v2
on. Both happen in one pass —
go run github.com/go-admin-team/go-admin-core/tools/coreupgrade@v2.0.0 -w -v2 .
go mod tidy
— which is the command the release notes give, run here as a consumer
would run it. 210 imports across 95 files.
The compatibility shims this used are gone in v2, so the paths that
moved had to move: sdk/pkg/captcha, sdk/pkg/jwtauth and its user
package, sdk/pkg/response and sdk/pkg/casbin.
The count of unformatted files is unchanged at 34, none of them touched
by this: the tool reformats a file only if it was already gofmt clean,
so a migration cannot disappear into whitespace.
opens is a map, so opens[c.Driver] on a driver this build does not carry
returns a nil function, and gorm.Open calls it. The operator saw a nil
dereference inside gorm with nothing naming the driver.
sqlite3 is the case that bites: it needs cgo and is only compiled in
under the sqlite3 build tag, so the same config file works on one binary
and dies on another. Resolve the driver first and say which ones this
build supports.
sys_user.username, sys_role.role_key and sys_dict_type.dict_type had no
unique index. Uniqueness was a SELECT COUNT followed by an INSERT, which
two concurrent requests both pass — and login resolves a username with
First, so which of the two accounts answers is whichever the database
returns.
The index cannot be on the key alone, because a soft-deleted row keeps
occupying the name and a deleted user's username could never be used
again. It has to include the delete marker, and the marker has to be
non-null: two live rows are (alice, NULL) and (alice, NULL), and NULL is
not equal to NULL, so an index over a nullable marker admits both. That
is the worst of the three states — a constraint that reads as protection
and binds nothing — and there is a test that demonstrates it rather than
asserting it.
ModelTime.DeletedAt is milliseconds since the epoch now, zero while the
row is live. Sixteen tables carry it; the migration converts each one,
preserving when each deleted row was deleted, then adds the three
indexes.
Written to be re-runnable rather than transactional, because DDL does not
roll back on MySQL and an operator whose first attempt failed halfway
should have nothing to do but run it again. It refuses before altering
anything if a table already holds duplicates, naming them, rather than
letting the index fail and leaving the operator to guess.
The timestamp conversion happens in Go: turning a timestamp into epoch
milliseconds is spelled differently by every dialect this supports, and
these row counts do not justify four versions of it.
The pinned core dated from April, before sdk stopped being a separate module,
so the build resolved sdk packages from the old module and core packages from
the new one. Dropping the separate requirement is what makes the two agree
again.
Most of the diff is renames that came with that: the tenant accessors gained a
ByTenant suffix, GetDb now returns one database and GetAllDb the map, and
casbin moved to v3.
The change that matters is four call sites moving from GetMemoryQueue to
GetQueuePrefix. GetMemoryQueue returns a queue fixed at construction, so the
login log, the operate log and the api check ran in process no matter what the
settings file selected — a second instance saw none of it. GetQueuePrefix
returns whatever the configuration built, which is the point of being able to
configure a queue at all.
Verified against core at main: build and vet clean. The two file_store failures
are unchanged from before this branch; they need cloud credentials.