feat(server): Loader/Recorder на Store, wiring cmd/server (config→migrate→pool→api)
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/api"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/config"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/crypto"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/provider/registry"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/provider/selectel"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/service"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("config: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.Migrate(ctx, cfg.DBDSN); err != nil {
|
||||||
|
log.Fatalf("migrate: %v", err)
|
||||||
|
}
|
||||||
|
pool, err := pgxpool.New(ctx, cfg.DBDSN)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("pool: %v", err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
cipher, err := crypto.NewCipher(cfg.EncKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cipher: %v", err)
|
||||||
|
}
|
||||||
|
st := store.New(pool)
|
||||||
|
|
||||||
|
reg := registry.New()
|
||||||
|
reg.Register(selectel.New())
|
||||||
|
|
||||||
|
svc := service.New(st, st, reg, cipher)
|
||||||
|
a := &api.API{Svc: svc}
|
||||||
|
|
||||||
|
log.Printf("listening on %s", cfg.ListenAddr)
|
||||||
|
if err := http.ListenAndServe(cfg.ListenAddr, api.NewRouter(a)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
dto "github.com/vasyakrg/dns-autoresolver/internal/store/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createDomain = `-- name: CreateDomain :one
|
const createDomain = `-- name: CreateDomain :one
|
||||||
@@ -117,3 +118,30 @@ func (q *Queries) ListDomains(ctx context.Context, projectID uuid.UUID) ([]Domai
|
|||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadDomainFull = `-- name: LoadDomainFull :one
|
||||||
|
SELECT d.zone_id, a.provider, a.secret_enc, t.doc
|
||||||
|
FROM domains d
|
||||||
|
JOIN provider_accounts a ON a.id = d.provider_account_id
|
||||||
|
LEFT JOIN templates t ON t.id = d.template_id
|
||||||
|
WHERE d.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type LoadDomainFullRow struct {
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
SecretEnc string `json:"secret_enc"`
|
||||||
|
Doc *dto.TemplateDoc `json:"doc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) LoadDomainFull(ctx context.Context, id uuid.UUID) (LoadDomainFullRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, loadDomainFull, id)
|
||||||
|
var i LoadDomainFullRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ZoneID,
|
||||||
|
&i.Provider,
|
||||||
|
&i.SecretEnc,
|
||||||
|
&i.Doc,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/diff"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/service"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/store/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadDomain joins domains+provider_accounts+templates to build the
|
||||||
|
// service.DomainRef needed to check/apply a domain's DNS records.
|
||||||
|
func (s *Store) LoadDomain(ctx context.Context, domainID uuid.UUID) (service.DomainRef, error) {
|
||||||
|
row, err := s.q.LoadDomainFull(ctx, domainID)
|
||||||
|
if err != nil {
|
||||||
|
return service.DomainRef{}, err
|
||||||
|
}
|
||||||
|
if row.Doc == nil {
|
||||||
|
return service.DomainRef{}, fmt.Errorf("store: domain %s has no template", domainID)
|
||||||
|
}
|
||||||
|
return service.DomainRef{
|
||||||
|
ZoneID: row.ZoneID,
|
||||||
|
Provider: row.Provider,
|
||||||
|
SecretEnc: row.SecretEnc,
|
||||||
|
Template: *row.Doc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCheckRun persists a summary of the changeset (counts of updates/prunes)
|
||||||
|
// as a check_runs row.
|
||||||
|
func (s *Store) SaveCheckRun(ctx context.Context, domainID uuid.UUID, cs diff.Changeset) error {
|
||||||
|
summary := map[string]int{
|
||||||
|
"updates": len(cs.Updates()),
|
||||||
|
"prunes": len(cs.Prunes()),
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(summary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = s.q.CreateCheckRun(ctx, db.CreateCheckRunParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
DomainID: domainID,
|
||||||
|
Result: raw,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile-time interface checks
|
||||||
|
var _ service.Loader = (*Store)(nil)
|
||||||
|
var _ service.Recorder = (*Store)(nil)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/diff"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/model"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/store/db"
|
||||||
|
"github.com/vasyakrg/dns-autoresolver/internal/store/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadDomainAndSaveCheckRun(t *testing.T) {
|
||||||
|
s, ctx := newStore(t)
|
||||||
|
|
||||||
|
acc, err := s.Queries().CreateAccount(ctx, db.CreateAccountParams{
|
||||||
|
ID: uuid.New(), ProjectID: defaultProject,
|
||||||
|
Provider: "selectel", SecretEnc: "enc-blob", Comment: "prod",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := dto.TemplateDoc{Records: []dto.RecordDTO{
|
||||||
|
{Type: "A", Name: "www.example.com.", TTL: 300, Values: []string{"1.2.3.4"}},
|
||||||
|
}}
|
||||||
|
tpl, err := s.Queries().CreateTemplate(ctx, db.CreateTemplateParams{
|
||||||
|
ID: uuid.New(), ProjectID: defaultProject, Name: "base", Doc: &doc,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := s.Queries().CreateDomain(ctx, db.CreateDomainParams{
|
||||||
|
ID: uuid.New(), ProjectID: defaultProject, ProviderAccountID: acc.ID,
|
||||||
|
ZoneName: "example.com", ZoneID: "zone-1", TemplateID: &tpl.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := s.LoadDomain(ctx, domain.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ref.ZoneID != "zone-1" || ref.Provider != "selectel" || ref.SecretEnc != "enc-blob" {
|
||||||
|
t.Fatalf("domain ref mismatch: %+v", ref)
|
||||||
|
}
|
||||||
|
if len(ref.Template.Records) != 1 {
|
||||||
|
t.Fatalf("expected template records, got %+v", ref.Template)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := diff.Changeset{Diffs: []diff.RecordDiff{
|
||||||
|
{Kind: diff.Add, Type: model.A, Name: "www.example.com."},
|
||||||
|
{Kind: diff.Delete, Type: model.A, Name: "old.example.com."},
|
||||||
|
}}
|
||||||
|
if err := s.SaveCheckRun(ctx, domain.ID, cs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
if err := s.pool.QueryRow(ctx, "SELECT count(*) FROM check_runs WHERE domain_id = $1", domain.ID).Scan(&count); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatalf("expected 1 check_runs row, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDomainNoTemplate(t *testing.T) {
|
||||||
|
s, ctx := newStore(t)
|
||||||
|
|
||||||
|
acc, err := s.Queries().CreateAccount(ctx, db.CreateAccountParams{
|
||||||
|
ID: uuid.New(), ProjectID: defaultProject,
|
||||||
|
Provider: "selectel", SecretEnc: "enc-blob", Comment: "prod",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := s.Queries().CreateDomain(ctx, db.CreateDomainParams{
|
||||||
|
ID: uuid.New(), ProjectID: defaultProject, ProviderAccountID: acc.ID,
|
||||||
|
ZoneName: "example.com", ZoneID: "zone-2", TemplateID: nil,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.LoadDomain(ctx, domain.ID); err == nil {
|
||||||
|
t.Fatal("expected error for domain without template, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,3 +11,10 @@ SELECT * FROM domains WHERE project_id = $1 ORDER BY created_at;
|
|||||||
|
|
||||||
-- name: DeleteDomain :exec
|
-- name: DeleteDomain :exec
|
||||||
DELETE FROM domains WHERE id = $1 AND project_id = $2;
|
DELETE FROM domains WHERE id = $1 AND project_id = $2;
|
||||||
|
|
||||||
|
-- name: LoadDomainFull :one
|
||||||
|
SELECT d.zone_id, a.provider, a.secret_enc, t.doc
|
||||||
|
FROM domains d
|
||||||
|
JOIN provider_accounts a ON a.id = d.provider_account_id
|
||||||
|
LEFT JOIN templates t ON t.id = d.template_id
|
||||||
|
WHERE d.id = $1;
|
||||||
|
|||||||
Reference in New Issue
Block a user