Skip to content

Commit 00a91da

Browse files
committed
feat(backup): self-contained scheduled accessory backups (--local --app)
The scheduled cron line used to reference plain `teploy accessory backup`, which needs a teploy.yml cwd that doesn't exist server-side — so every scheduled accessory backup died at config load. Now: - --local runs against the local docker host (LocalExecutor, no SSH), and --app + InspectAccessory read the accessory's image/env from the running container — no teploy.yml needed. This is the mode the cron job uses; it works interactively too. - --schedule writes a self-contained cron line (--local --app + embedded endpoint/creds when set) and ships the teploy binary to the server via the existing checksum-verified release pipeline (same as heal/autodeploy setup). Validated live on infra-home: the exact crontab line executed server-side end-to-end (dump -> MinIO upload). NOTE: the shipped binary is the latest RELEASE; --local reaches servers with cli >= v0.1.20 — earlier releases fail loudly at cron time (unknown flag), not silently.
1 parent 5ebd564 commit 00a91da

1 file changed

Lines changed: 63 additions & 17 deletions

File tree

‎internal/cli/accessory.go‎

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -336,34 +336,51 @@ func newAccessoryBackupCmd(flags *Flags) *cobra.Command {
336336
region string
337337
endpoint string
338338
schedule string
339+
appName string
340+
local bool
339341
)
340342

341343
cmd := &cobra.Command{
342344
Use: "backup <name>",
343345
Short: "Back up an accessory (database-aware dump)",
344346
Args: cobra.ExactArgs(1),
345347
RunE: func(cmd *cobra.Command, args []string) error {
346-
return runAccessoryBackup(flags, args[0], bucket, region, endpoint, schedule)
348+
return runAccessoryBackup(flags, appName, args[0], bucket, region, endpoint, schedule, local)
347349
},
348350
}
349351

350352
cmd.Flags().StringVar(&bucket, "bucket", "", "S3 bucket name")
351353
cmd.Flags().StringVar(&region, "region", "us-east-1", "AWS region")
352354
cmd.Flags().StringVar(&endpoint, "endpoint", "", "S3-compatible endpoint URL (MinIO/B2/R2); creds from TEPLOY_S3_ACCESS_KEY/SECRET_KEY or AWS_* env")
353355
cmd.Flags().StringVar(&schedule, "schedule", "", "cron schedule for automated backups")
356+
cmd.Flags().StringVar(&appName, "app", "", "app name — read accessory image/env from server state instead of teploy.yml")
357+
cmd.Flags().BoolVar(&local, "local", false, "run against the local docker host (the mode scheduled cron jobs use; requires --app)")
354358

355359
return cmd
356360
}
357361

358-
func runAccessoryBackup(flags *Flags, name, bucket, region, endpoint, schedule string) error {
359-
appCfg, err := loadAppCfgForAccessory()
360-
if err != nil {
361-
return err
362+
func runAccessoryBackup(flags *Flags, appName, name, bucket, region, endpoint, schedule string, local bool) error {
363+
if local && appName == "" {
364+
return fmt.Errorf("--local requires --app (no teploy.yml exists on the server)")
362365
}
363366

364-
accCfg, ok := appCfg.Accessories[name]
365-
if !ok {
366-
return fmt.Errorf("accessory %q not found in teploy.yml", name)
367+
var app string
368+
var accImage string
369+
var accEnv map[string]string
370+
if !local {
371+
appCfg, err := loadAppCfgForAccessory()
372+
if err != nil {
373+
return err
374+
}
375+
accCfg, ok := appCfg.Accessories[name]
376+
if !ok {
377+
return fmt.Errorf("accessory %q not found in teploy.yml", name)
378+
}
379+
app = appCfg.App
380+
accImage = accCfg.Image
381+
accEnv = accCfg.Env
382+
} else {
383+
app = appName
367384
}
368385

369386
if bucket == "" {
@@ -384,32 +401,61 @@ func runAccessoryBackup(flags *Flags, name, bucket, region, endpoint, schedule s
384401
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
385402
defer cancel()
386403

387-
executor, err := connectForApp(ctx, flags, appCfg)
388-
if err != nil {
389-
return err
404+
// --local runs on the server itself (the mode scheduled cron jobs use):
405+
// docker + aws execute directly, no SSH hop, no teploy.yml — the
406+
// accessory's image and env are read from the running container.
407+
var executor ssh.Executor
408+
if local {
409+
executor = ssh.NewLocalExecutor()
410+
} else {
411+
appCfg, err := loadAppCfgForAccessory()
412+
if err != nil {
413+
return err
414+
}
415+
executor, err = connectForApp(ctx, flags, appCfg)
416+
if err != nil {
417+
return err
418+
}
390419
}
391420
defer executor.Close()
392421

393422
client := backup.NewClient(executor, os.Stdout)
394423
s3Cfg := s3Config(bucket, region, endpoint)
395424

425+
if local {
426+
image, env, err := client.InspectAccessory(ctx, app, name)
427+
if err != nil {
428+
return err
429+
}
430+
accImage, accEnv = image, env
431+
}
432+
396433
if schedule != "" {
397-
backupCmd := fmt.Sprintf("teploy accessory backup %s --bucket %s --region %s", name, bucket, region)
434+
// The scheduled job runs ON the server: --local --app makes it
435+
// self-contained (no teploy.yml server-side), reading accessory
436+
// config from the running container.
437+
backupCmd := fmt.Sprintf("teploy accessory backup %s --local --app %s --bucket %s --region %s",
438+
name, app, bucket, region)
398439
if endpoint != "" {
399-
// The cron job runs server-side where the local TEPLOY_S3_* env
400-
// doesn't exist — embed endpoint + creds in the crontab line
401-
// (root-only readable, same trust class as ~/.aws/credentials).
440+
// Cron has no TEPLOY_S3_* env — embed endpoint + creds in the
441+
// crontab line (root-only readable, same trust class as
442+
// ~/.aws/credentials).
402443
backupCmd = fmt.Sprintf("TEPLOY_S3_ACCESS_KEY=%s TEPLOY_S3_SECRET_KEY=%s %s --endpoint %s",
403444
ssh.ShellQuote(s3Cfg.AccessKey), ssh.ShellQuote(s3Cfg.SecretKey), backupCmd, ssh.ShellQuote(endpoint))
404445
}
405-
if err := client.SetSchedule(ctx, schedule, backupCmd, "teploy-accessory-backup:"+appCfg.App+":"+name); err != nil {
446+
// The cron job needs the teploy binary on the server; ship it via
447+
// the existing checksum-verified pipeline (same as heal/autodeploy).
448+
if _, err := deployTeployBinaryToServer(ctx, executor, "/usr/local/bin/teploy"); err != nil {
449+
return fmt.Errorf("installing teploy binary for the scheduled job: %w", err)
450+
}
451+
if err := client.SetSchedule(ctx, schedule, backupCmd, "teploy-accessory-backup:"+app+":"+name); err != nil {
406452
return err
407453
}
408454
fmt.Printf("Scheduled backup: %s\n", schedule)
409455
return nil
410456
}
411457

412-
return client.AccessoryBackup(ctx, appCfg.App, name, accCfg.Image, accCfg.Env, s3Cfg)
458+
return client.AccessoryBackup(ctx, app, name, accImage, accEnv, s3Cfg)
413459
}
414460

415461
func newAccessoryRestoreCmd(flags *Flags) *cobra.Command {

0 commit comments

Comments
 (0)