buddy fix
This commit is contained in:
@@ -1,12 +1,16 @@
|
|||||||
# Nothing in here reaches the image.
|
# What does not reach the image.
|
||||||
#
|
#
|
||||||
# `.env*` most of all: the Dockerfile does `COPY . .`, and the production
|
# `.env.production` IS copied in — see the Dockerfile's runtime stage. The
|
||||||
# credentials in `.env.production` were being baked into every image built
|
# container reads its own configuration from that file, so the deployment does
|
||||||
# from this folder. The running container gets its environment from the
|
# not depend on every variable having been typed into a hosting platform's
|
||||||
# platform (Dokploy / Kubernetes), never from a file.
|
# settings screen. Anything the platform DOES set still wins: godotenv never
|
||||||
|
# overwrites a variable that is already present in the environment.
|
||||||
|
#
|
||||||
|
# Every other `.env.*` stays out. `.env.local` and `.env.secrets` are one
|
||||||
|
# developer's machine, and `.env.secrets` in particular is the file that holds
|
||||||
|
# a key — it must never be inside an image.
|
||||||
|
.env*
|
||||||
|
!.env.production
|
||||||
|
|
||||||
.git
|
.git
|
||||||
.claude
|
.claude
|
||||||
|
|||||||
@@ -78,3 +78,7 @@ REDIS_DB=0
|
|||||||
|
|
||||||
# ── Auth ────────────────────────────────────────────────────────────────────
|
# ── Auth ────────────────────────────────────────────────────────────────────
|
||||||
POS_TOKEN_SECRET=XCYrH7J6pi0wGzufaYfIXialqRVzlLRslaTlDbhfqQQl
|
POS_TOKEN_SECRET=XCYrH7J6pi0wGzufaYfIXialqRVzlLRslaTlDbhfqQQl
|
||||||
|
|
||||||
|
# ── Nearle Buddy ────────────────────────────────────────────────────────────
|
||||||
|
# One variable. Provider, endpoint and model are constants in config.go.
|
||||||
|
ASSISTANT_API_KEY=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
|
||||||
|
|||||||
20
Dockerfile
20
Dockerfile
@@ -28,10 +28,22 @@ WORKDIR /app
|
|||||||
COPY --from=builder /app/server /app
|
COPY --from=builder /app/server /app
|
||||||
COPY nearle-gear-firebase-adminsdk-l9oha-23ca3b3609.json .
|
COPY nearle-gear-firebase-adminsdk-l9oha-23ca3b3609.json .
|
||||||
|
|
||||||
# No `.env.*` is copied in (see .dockerignore), so this only decides which
|
# The container's own configuration.
|
||||||
# rules config.Load applies: production insists on a signing secret and never
|
#
|
||||||
# falls back to localhost values. Every real value comes from the platform's
|
# Copied in so the deployment does not depend on every variable having been
|
||||||
# environment settings.
|
# entered into a hosting platform's settings screen. A variable missed there is
|
||||||
|
# a variable unset in production, and the failure is silent — the assistant sat
|
||||||
|
# switched off for two days for exactly that reason, with nothing on any screen
|
||||||
|
# saying which value was absent.
|
||||||
|
#
|
||||||
|
# Anything the platform DOES set still wins: `godotenv` only fills variables
|
||||||
|
# that are not already in the environment, so Dokploy can override any line in
|
||||||
|
# this file without the file having to change.
|
||||||
|
COPY .env.production .
|
||||||
|
|
||||||
|
# Decides which rules config.Load applies — production insists on a signing
|
||||||
|
# secret and never falls back to localhost values — and which file above is
|
||||||
|
# read: loadEnvFiles reads `.env.<APP_ENV>` then `.env`.
|
||||||
ENV APP_ENV=production
|
ENV APP_ENV=production
|
||||||
|
|
||||||
# Must match APP_PORT in the platform's environment (1009 in production).
|
# Must match APP_PORT in the platform's environment (1009 in production).
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -206,3 +207,55 @@ func TestEachDefaultIsStillOverridable(t *testing.T) {
|
|||||||
t.Fatalf("a local model was refused: %s", cfg.Why())
|
t.Fatalf("a local model was refused: %s", cfg.Why())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The container reads its own configuration from a file beside the binary.
|
||||||
|
//
|
||||||
|
// The Dockerfile copies `.env.production` into the runtime image and sets
|
||||||
|
// APP_ENV=production, so `loadEnvFiles` reads it on boot. This asserts the
|
||||||
|
// mechanism rather than the Dockerfile — a COPY line is easy to check by eye
|
||||||
|
// and easy to believe wrongly, and the failure it produces is a server that
|
||||||
|
// starts fine with a variable silently unset.
|
||||||
|
func TestTheEnvironmentFileBesideTheBinaryIsRead(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
|
||||||
|
if err := os.WriteFile(".env.production",
|
||||||
|
[]byte("ASSISTANT_API_KEY=from-the-file\n"), 0o600); err != nil {
|
||||||
|
t.Fatalf("writing the fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("APP_ENV", "production")
|
||||||
|
// Registered with t.Setenv first so it is restored on return, then removed:
|
||||||
|
// godotenv does not overwrite a variable that is PRESENT, and an empty
|
||||||
|
// string is present. Setting it to "" would have tested nothing.
|
||||||
|
t.Setenv("ASSISTANT_API_KEY", "placeholder")
|
||||||
|
os.Unsetenv("ASSISTANT_API_KEY")
|
||||||
|
|
||||||
|
loadEnvFiles()
|
||||||
|
|
||||||
|
if os.Getenv("ASSISTANT_API_KEY") != "from-the-file" {
|
||||||
|
t.Fatal("the environment file beside the binary was not read")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThePlatformStillWinsOverTheFile(t *testing.T) {
|
||||||
|
// godotenv never overwrites a variable already in the environment, so a
|
||||||
|
// value set on the hosting platform overrides the committed file without
|
||||||
|
// the file having to change. Both mechanisms work; neither fights the other.
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
|
||||||
|
if err := os.WriteFile(".env.production",
|
||||||
|
[]byte("ASSISTANT_API_KEY=from-the-file\n"), 0o600); err != nil {
|
||||||
|
t.Fatalf("writing the fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("APP_ENV", "production")
|
||||||
|
t.Setenv("ASSISTANT_API_KEY", "from-the-platform")
|
||||||
|
|
||||||
|
loadEnvFiles()
|
||||||
|
|
||||||
|
if got := os.Getenv("ASSISTANT_API_KEY"); got != "from-the-platform" {
|
||||||
|
t.Fatalf("the file overrode the platform: ASSISTANT_API_KEY=%q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
49
scratch/buddyconfig/main.go
Normal file
49
scratch/buddyconfig/main.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Would this server switch Nearle Buddy on?
|
||||||
|
//
|
||||||
|
// APP_ENV=production go run ./scratch/buddyconfig
|
||||||
|
//
|
||||||
|
// Reads the configuration exactly as `main.go` does — same files, same order,
|
||||||
|
// same defaults — and reports whether the assistant would be built. Prints a
|
||||||
|
// verdict and, when the answer is no, the name of the variable that is missing.
|
||||||
|
// Never a value: this exists to be run against production configuration.
|
||||||
|
//
|
||||||
|
// Connects to nothing. `config.Load` reads and validates; the database, the
|
||||||
|
// model and the queue are all somebody else's job.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"nearle/config"
|
||||||
|
"nearle/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("configuration is not valid:")
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("APP_ENV %s\n", cfg.AppEnv)
|
||||||
|
fmt.Printf("sessions can issue %t\n", utils.WebTokenConfigured())
|
||||||
|
|
||||||
|
if !cfg.Assistant.Enabled() {
|
||||||
|
fmt.Printf("assistant OFF — %s\n", cfg.Assistant.Why())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The gateway itself, built the way the facade builds it. Enabled() passing
|
||||||
|
// and NewChat returning nil would be a disagreement worth catching here
|
||||||
|
// rather than at the first question somebody asks.
|
||||||
|
chat, err := utils.NewChat(cfg.Assistant)
|
||||||
|
if err != nil || chat == nil {
|
||||||
|
fmt.Printf("assistant OFF — gateway not built: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("assistant ON — provider %s, balanced tier %s\n",
|
||||||
|
cfg.Assistant.Provider, cfg.Assistant.ModelFor(utils.TierBalanced))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user