mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
chore: cleanup and route refactoring
Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
97
cmd/serve.go
97
cmd/serve.go
@@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/glueops/autoglue/internal/auth"
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
"github.com/glueops/autoglue/internal/bg"
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
"github.com/glueops/autoglue/internal/config"
|
"github.com/glueops/autoglue/internal/config"
|
||||||
"github.com/glueops/autoglue/internal/web"
|
"github.com/glueops/autoglue/internal/models"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -34,13 +34,13 @@ var serveCmd = &cobra.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pgwebInst *web.Pgweb
|
|
||||||
|
|
||||||
jobs, err := bg.NewJobs(rt.DB, cfg.DbURL)
|
jobs, err := bg.NewJobs(rt.DB, cfg.DbURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to init background jobs: %v", err)
|
log.Fatalf("failed to init background jobs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rt.DB.Where("status IN ?", []string{"scheduled", "queued", "pending"}).Delete(&models.Job{})
|
||||||
|
|
||||||
// Start workers in background ONCE
|
// Start workers in background ONCE
|
||||||
go func() {
|
go func() {
|
||||||
if err := jobs.Start(); err != nil {
|
if err := jobs.Start(); err != nil {
|
||||||
@@ -53,7 +53,7 @@ var serveCmd = &cobra.Command{
|
|||||||
{
|
{
|
||||||
// schedule next 03:30 local time
|
// schedule next 03:30 local time
|
||||||
next := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 30*time.Minute)
|
next := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 30*time.Minute)
|
||||||
_, _ = jobs.Enqueue(
|
_, err = jobs.Enqueue(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
uuid.NewString(),
|
uuid.NewString(),
|
||||||
"archer_cleanup",
|
"archer_cleanup",
|
||||||
@@ -61,10 +61,13 @@ var serveCmd = &cobra.Command{
|
|||||||
archer.WithScheduleTime(next),
|
archer.WithScheduleTime(next),
|
||||||
archer.WithMaxRetries(1),
|
archer.WithMaxRetries(1),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to enqueue archer cleanup job: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// schedule next 03:45 local time
|
// schedule next 03:45 local time
|
||||||
next2 := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 45*time.Minute)
|
next2 := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 45*time.Minute)
|
||||||
_, _ = jobs.Enqueue(
|
_, err = jobs.Enqueue(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
uuid.NewString(),
|
uuid.NewString(),
|
||||||
"tokens_cleanup",
|
"tokens_cleanup",
|
||||||
@@ -72,45 +75,47 @@ var serveCmd = &cobra.Command{
|
|||||||
archer.WithScheduleTime(next2),
|
archer.WithScheduleTime(next2),
|
||||||
archer.WithMaxRetries(1),
|
archer.WithMaxRetries(1),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to enqueue token cleanup job: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = jobs.Enqueue(
|
_, err = jobs.Enqueue(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
uuid.NewString(),
|
uuid.NewString(),
|
||||||
"db_backup_s3",
|
"db_backup_s3",
|
||||||
bg.DbBackupArgs{},
|
bg.DbBackupArgs{IntervalS: 3600},
|
||||||
archer.WithMaxRetries(1),
|
archer.WithMaxRetries(1),
|
||||||
archer.WithScheduleTime(time.Now().Add(1*time.Hour)),
|
archer.WithScheduleTime(time.Now().Add(1*time.Hour)),
|
||||||
)
|
)
|
||||||
}
|
if err != nil {
|
||||||
|
log.Fatalf("failed to enqueue backup jobs: %v", err)
|
||||||
// Periodic scheduler
|
|
||||||
schedCtx, schedCancel := context.WithCancel(context.Background())
|
|
||||||
defer schedCancel()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
_, err := jobs.Enqueue(
|
|
||||||
context.Background(),
|
|
||||||
uuid.NewString(),
|
|
||||||
"bootstrap_bastion",
|
|
||||||
bg.BastionBootstrapArgs{},
|
|
||||||
archer.WithMaxRetries(3),
|
|
||||||
// while debugging, avoid extra schedule delay:
|
|
||||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
|
||||||
}
|
|
||||||
case <-schedCtx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
_, err = jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"dns_reconcile",
|
||||||
|
bg.DNSReconcileArgs{MaxDomains: 25, MaxRecords: 100, IntervalS: 10},
|
||||||
|
archer.WithScheduleTime(time.Now().Add(5*time.Second)),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to enqueue dns reconcile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"bootstrap_bastion",
|
||||||
|
bg.BastionBootstrapArgs{IntervalS: 10},
|
||||||
|
archer.WithMaxRetries(3),
|
||||||
|
// while debugging, avoid extra schedule delay:
|
||||||
|
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -121,7 +126,6 @@ var serveCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var studioHandler http.Handler
|
|
||||||
r := api.NewRouter(rt.DB, jobs, nil)
|
r := api.NewRouter(rt.DB, jobs, nil)
|
||||||
|
|
||||||
if cfg.DBStudioEnabled {
|
if cfg.DBStudioEnabled {
|
||||||
@@ -130,20 +134,16 @@ var serveCmd = &cobra.Command{
|
|||||||
dbURL = cfg.DbURL
|
dbURL = cfg.DbURL
|
||||||
}
|
}
|
||||||
|
|
||||||
pgwebInst, err = web.StartPgweb(
|
studio, err := api.PgwebHandler(
|
||||||
dbURL,
|
dbURL,
|
||||||
cfg.DBStudioBind,
|
"db-studio",
|
||||||
cfg.DBStudioPort,
|
false,
|
||||||
true,
|
|
||||||
cfg.DBStudioUser,
|
|
||||||
cfg.DBStudioPass,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("pgweb failed to start: %v", err)
|
log.Fatalf("failed to init db studio: %v", err)
|
||||||
} else {
|
} else {
|
||||||
studioHandler = http.HandlerFunc(pgwebInst.Proxy())
|
r = api.NewRouter(rt.DB, jobs, studio)
|
||||||
r = api.NewRouter(rt.DB, jobs, studioHandler)
|
log.Printf("pgweb mounted at /db-studio/")
|
||||||
log.Printf("pgweb running on http://%s:%s (proxied at /db-studio/)", cfg.DBStudioBind, pgwebInst.Port())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,9 +169,6 @@ var serveCmd = &cobra.Command{
|
|||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
fmt.Println("\n⏳ Shutting down...")
|
fmt.Println("\n⏳ Shutting down...")
|
||||||
if pgwebInst != nil {
|
|
||||||
_ = pgwebInst.Stop(context.Background())
|
|
||||||
}
|
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return srv.Shutdown(shutdownCtx)
|
return srv.Shutdown(shutdownCtx)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -158,6 +158,19 @@ definitions:
|
|||||||
- scope_version
|
- scope_version
|
||||||
- secret
|
- secret
|
||||||
type: object
|
type: object
|
||||||
|
dto.CreateDomainRequest:
|
||||||
|
properties:
|
||||||
|
credential_id:
|
||||||
|
type: string
|
||||||
|
domain_name:
|
||||||
|
type: string
|
||||||
|
zone_id:
|
||||||
|
maxLength: 128
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- credential_id
|
||||||
|
- domain_name
|
||||||
|
type: object
|
||||||
dto.CreateLabelRequest:
|
dto.CreateLabelRequest:
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
@@ -175,6 +188,28 @@ definitions:
|
|||||||
- worker
|
- worker
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.CreateRecordSetRequest:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: |-
|
||||||
|
Name relative to domain ("endpoint") OR FQDN ("endpoint.example.com").
|
||||||
|
Server normalizes to relative.
|
||||||
|
maxLength: 253
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
maximum: 86400
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
values:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
dto.CreateSSHRequest:
|
dto.CreateSSHRequest:
|
||||||
properties:
|
properties:
|
||||||
bits:
|
bits:
|
||||||
@@ -253,6 +288,27 @@ definitions:
|
|||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.DomainResponse:
|
||||||
|
properties:
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
credential_id:
|
||||||
|
type: string
|
||||||
|
domain_name:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
last_error:
|
||||||
|
type: string
|
||||||
|
organization_id:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
zone_id:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.EnqueueRequest:
|
dto.EnqueueRequest:
|
||||||
properties:
|
properties:
|
||||||
payload:
|
payload:
|
||||||
@@ -440,6 +496,34 @@ definitions:
|
|||||||
example: 7
|
example: 7
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
dto.RecordSetResponse:
|
||||||
|
properties:
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
domain_id:
|
||||||
|
type: string
|
||||||
|
fingerprint:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
last_error:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
owner:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
values:
|
||||||
|
description: '[]string JSON'
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
dto.RefreshRequest:
|
dto.RefreshRequest:
|
||||||
properties:
|
properties:
|
||||||
refresh_token:
|
refresh_token:
|
||||||
@@ -575,6 +659,23 @@ definitions:
|
|||||||
description: set if rotating
|
description: set if rotating
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
|
dto.UpdateDomainRequest:
|
||||||
|
properties:
|
||||||
|
credential_id:
|
||||||
|
type: string
|
||||||
|
domain_name:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
enum:
|
||||||
|
- pending
|
||||||
|
- provisioning
|
||||||
|
- ready
|
||||||
|
- failed
|
||||||
|
type: string
|
||||||
|
zone_id:
|
||||||
|
maxLength: 128
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.UpdateLabelRequest:
|
dto.UpdateLabelRequest:
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
@@ -592,6 +693,30 @@ definitions:
|
|||||||
- worker
|
- worker
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.UpdateRecordSetRequest:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Any change flips status back to pending (worker will UPSERT)
|
||||||
|
maxLength: 253
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
enum:
|
||||||
|
- pending
|
||||||
|
- provisioning
|
||||||
|
- ready
|
||||||
|
- failed
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
maximum: 86400
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
values:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
dto.UpdateServerRequest:
|
dto.UpdateServerRequest:
|
||||||
properties:
|
properties:
|
||||||
hostname:
|
hostname:
|
||||||
@@ -1812,6 +1937,406 @@ paths:
|
|||||||
summary: Reveal decrypted secret (one-time read)
|
summary: Reveal decrypted secret (one-time read)
|
||||||
tags:
|
tags:
|
||||||
- Credentials
|
- Credentials
|
||||||
|
/dns/domains:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 'Returns domains for X-Org-ID. Filters: `domain_name`, `status`,
|
||||||
|
`q` (contains).'
|
||||||
|
operationId: ListDomains
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Exact domain name (lowercase, no trailing dot)
|
||||||
|
in: query
|
||||||
|
name: domain_name
|
||||||
|
type: string
|
||||||
|
- description: pending|provisioning|ready|failed
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
type: string
|
||||||
|
- description: Domain contains (case-insensitive)
|
||||||
|
in: query
|
||||||
|
name: q
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.DomainResponse'
|
||||||
|
type: array
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: db error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: List domains (org scoped)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Creates a domain bound to a Route 53 scoped credential. Archer
|
||||||
|
will backfill ZoneID if omitted.
|
||||||
|
operationId: CreateDomain
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Domain payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.CreateDomainRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.DomainResponse'
|
||||||
|
"400":
|
||||||
|
description: validation error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: db error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Create a domain (org scoped)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
/dns/domains/{domain_id}/records:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 'Filters: `name`, `type`, `status`.'
|
||||||
|
operationId: ListRecordSets
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Domain ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: domain_id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Exact relative name or FQDN (server normalizes)
|
||||||
|
in: query
|
||||||
|
name: name
|
||||||
|
type: string
|
||||||
|
- description: RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)
|
||||||
|
in: query
|
||||||
|
name: type
|
||||||
|
type: string
|
||||||
|
- description: pending|provisioning|ready|failed
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.RecordSetResponse'
|
||||||
|
type: array
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: domain not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: List record sets for a domain
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
operationId: CreateRecordSet
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Domain ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: domain_id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Record set payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.CreateRecordSetRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.RecordSetResponse'
|
||||||
|
"400":
|
||||||
|
description: validation error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: domain not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Create a record set (pending; Archer will UPSERT to Route 53)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
/dns/domains/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
operationId: DeleteDomain
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Domain ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No Content
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Delete a domain
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
operationId: GetDomain
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Domain ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.DomainResponse'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Get a domain (org scoped)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
patch:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
operationId: UpdateDomain
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Domain ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Fields to update
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.UpdateDomainRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.DomainResponse'
|
||||||
|
"400":
|
||||||
|
description: validation error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Update a domain (org scoped)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
/dns/records/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
operationId: DeleteRecordSet
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Record Set ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No Content
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Delete a record set (API removes row; worker can optionally handle
|
||||||
|
external deletion policy)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
|
patch:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
operationId: UpdateRecordSet
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Record Set ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Fields to update
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.UpdateRecordSetRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.RecordSetResponse'
|
||||||
|
"400":
|
||||||
|
description: validation error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Update a record set (flips to pending for reconciliation)
|
||||||
|
tags:
|
||||||
|
- DNS
|
||||||
/healthz:
|
/healthz:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -3621,6 +4146,57 @@ paths:
|
|||||||
summary: Update server (org scoped)
|
summary: Update server (org scoped)
|
||||||
tags:
|
tags:
|
||||||
- Servers
|
- Servers
|
||||||
|
/servers/{id}/reset-hostkey:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Clears the stored SSH host key for this server. The next SSH connection
|
||||||
|
will re-learn the host key (trust-on-first-use).
|
||||||
|
operationId: ResetServerHostKey
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
type: string
|
||||||
|
- description: Server ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ServerResponse'
|
||||||
|
"400":
|
||||||
|
description: invalid id
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: organization required
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: reset failed
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Reset SSH host key (org scoped)
|
||||||
|
tags:
|
||||||
|
- Servers
|
||||||
/ssh:
|
/ssh:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
39
go.mod
39
go.mod
@@ -7,9 +7,11 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2 v1.39.6
|
github.com/aws/aws-sdk-go-v2 v1.39.6
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.18
|
github.com/aws/aws-sdk-go-v2/config v1.31.18
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.22
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0
|
github.com/coreos/go-oidc/v3 v3.16.0
|
||||||
github.com/dyaksa/archer v1.1.3
|
github.com/dyaksa/archer v1.1.3
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/httprate v0.15.0
|
github.com/go-chi/httprate v0.15.0
|
||||||
@@ -18,6 +20,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/sosedoff/pgweb v0.16.2
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||||
@@ -32,7 +35,9 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
||||||
@@ -47,8 +52,15 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
github.com/aws/smithy-go v1.23.2 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
@@ -59,23 +71,38 @@ require (
|
|||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
@@ -84,12 +111,20 @@ require (
|
|||||||
github.com/sv-tools/openapi v0.2.1 // indirect
|
github.com/sv-tools/openapi v0.2.1 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||||
github.com/swaggo/swag v1.8.1 // indirect
|
github.com/swaggo/swag v1.8.1 // indirect
|
||||||
|
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gorm.io/driver/mysql v1.5.6 // indirect
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
113
go.sum
113
go.sum
@@ -1,9 +1,13 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||||
|
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
|
||||||
|
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
||||||
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
@@ -34,6 +38,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY
|
|||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4 h1:KEszjusgJ2dAqE5nSJY+5AHBkakfah8Sx6Vk3pjgrq8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4/go.mod h1:TUbfYOisWZWyT2qjmlMh93ERw1Ry8G4q/yT2Q8TsDag=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||||
@@ -44,6 +50,16 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJA
|
|||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
@@ -52,6 +68,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||||
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
||||||
github.com/dyaksa/archer v1.1.3 h1:jfe51tSNzzscFpu+Vilm4SKb0Lnv6FR1yaGspjab4x4=
|
github.com/dyaksa/archer v1.1.3 h1:jfe51tSNzzscFpu+Vilm4SKb0Lnv6FR1yaGspjab4x4=
|
||||||
github.com/dyaksa/archer v1.1.3/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4=
|
github.com/dyaksa/archer v1.1.3/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
@@ -60,6 +78,10 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
@@ -89,6 +111,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
@@ -96,6 +119,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
|||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
@@ -103,8 +128,9 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
|||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -117,16 +143,22 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
|||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -137,6 +169,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
@@ -147,12 +180,23 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
|
|||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||||
@@ -162,14 +206,30 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||||
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
|
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||||
|
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||||
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/sosedoff/pgweb v0.16.2 h1:1F1CWlCLSEgSctMva+nYuUibdhyiCUzlXyU5MQUJbFM=
|
||||||
|
github.com/sosedoff/pgweb v0.16.2/go.mod h1:ER7fsBddI3h7MQKO5RsUPi7Q/PWZYSKcI61kTp369Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -189,6 +249,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -208,42 +269,56 @@ github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
|||||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||||
github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
|
github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
|
||||||
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
|
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
|
||||||
|
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 h1:yL0l/u242MzDP6D0B5vGC+wxm5WRY+alQZy+dJk3bFI=
|
||||||
|
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948/go.mod h1:a06d/M1pxWi51qiSrfGMHaEydtuXT06nha8N2aNQuXk=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -251,8 +326,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
@@ -260,27 +333,25 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
|||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
26
internal/api/mount_admin_routes.go
Normal file
26
internal/api/mount_admin_routes.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountAdminRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authUser func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/admin", func(admin chi.Router) {
|
||||||
|
admin.Route("/archer", func(archer chi.Router) {
|
||||||
|
archer.Use(authUser)
|
||||||
|
archer.Use(httpmiddleware.RequirePlatformAdmin())
|
||||||
|
|
||||||
|
archer.Get("/jobs", handlers.AdminListArcherJobs(db))
|
||||||
|
archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs))
|
||||||
|
archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db))
|
||||||
|
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
||||||
|
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
20
internal/api/mount_annotation_routes.go
Normal file
20
internal/api/mount_annotation_routes.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountAnnotationRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/annotations", func(a chi.Router) {
|
||||||
|
a.Use(authOrg)
|
||||||
|
a.Get("/", handlers.ListAnnotations(db))
|
||||||
|
a.Post("/", handlers.CreateAnnotation(db))
|
||||||
|
a.Get("/{id}", handlers.GetAnnotation(db))
|
||||||
|
a.Patch("/{id}", handlers.UpdateAnnotation(db))
|
||||||
|
a.Delete("/{id}", handlers.DeleteAnnotation(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
37
internal/api/mount_api_routes.go
Normal file
37
internal/api/mount_api_routes.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountAPIRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs) {
|
||||||
|
r.Route("/api", func(api chi.Router) {
|
||||||
|
api.Route("/v1", func(v1 chi.Router) {
|
||||||
|
authUser := httpmiddleware.AuthMiddleware(db, false)
|
||||||
|
authOrg := httpmiddleware.AuthMiddleware(db, true)
|
||||||
|
|
||||||
|
// shared basics
|
||||||
|
mountMetaRoutes(v1)
|
||||||
|
mountAuthRoutes(v1, db)
|
||||||
|
|
||||||
|
// admin
|
||||||
|
mountAdminRoutes(v1, db, jobs, authUser)
|
||||||
|
|
||||||
|
// user/org scoped
|
||||||
|
mountMeRoutes(v1, db, authUser)
|
||||||
|
mountOrgRoutes(v1, db, authUser, authOrg)
|
||||||
|
|
||||||
|
mountCredentialRoutes(v1, db, authOrg)
|
||||||
|
mountSSHRoutes(v1, db, authOrg)
|
||||||
|
mountServerRoutes(v1, db, authOrg)
|
||||||
|
mountTaintRoutes(v1, db, authOrg)
|
||||||
|
mountLabelRoutes(v1, db, authOrg)
|
||||||
|
mountAnnotationRoutes(v1, db, authOrg)
|
||||||
|
mountNodePoolRoutes(v1, db, authOrg)
|
||||||
|
mountDNSRoutes(v1, db, authOrg)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
16
internal/api/mount_auth_routes.go
Normal file
16
internal/api/mount_auth_routes.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountAuthRoutes(r chi.Router, db *gorm.DB) {
|
||||||
|
r.Route("/auth", func(a chi.Router) {
|
||||||
|
a.Post("/{provider}/start", handlers.AuthStart(db))
|
||||||
|
a.Get("/{provider}/callback", handlers.AuthCallback(db))
|
||||||
|
a.Post("/refresh", handlers.Refresh(db))
|
||||||
|
a.Post("/logout", handlers.Logout(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
21
internal/api/mount_credential_routes.go
Normal file
21
internal/api/mount_credential_routes.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountCredentialRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/credentials", func(c chi.Router) {
|
||||||
|
c.Use(authOrg)
|
||||||
|
c.Get("/", handlers.ListCredentials(db))
|
||||||
|
c.Post("/", handlers.CreateCredential(db))
|
||||||
|
c.Get("/{id}", handlers.GetCredential(db))
|
||||||
|
c.Patch("/{id}", handlers.UpdateCredential(db))
|
||||||
|
c.Delete("/{id}", handlers.DeleteCredential(db))
|
||||||
|
c.Post("/{id}/reveal", handlers.RevealCredential(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
53
internal/api/mount_db_studio.go
Normal file
53
internal/api/mount_db_studio.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
pgapi "github.com/sosedoff/pgweb/pkg/api"
|
||||||
|
pgclient "github.com/sosedoff/pgweb/pkg/client"
|
||||||
|
pgcmd "github.com/sosedoff/pgweb/pkg/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PgwebHandler(dbURL, prefix string, readonly bool) (http.Handler, error) {
|
||||||
|
// Normalize prefix for pgweb:
|
||||||
|
// - no leading slash
|
||||||
|
// - always trailing slash if not empty
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = prefix + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
pgcmd.Opts = pgcmd.Options{
|
||||||
|
URL: dbURL,
|
||||||
|
Prefix: prefix, // e.g. "db-studio/"
|
||||||
|
ReadOnly: readonly,
|
||||||
|
Sessions: false,
|
||||||
|
LockSession: true,
|
||||||
|
SkipOpen: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := pgclient.NewFromUrl(dbURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if readonly {
|
||||||
|
_ = cli.SetReadOnlyMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cli.Test(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pgapi.DbClient = cli
|
||||||
|
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
g := gin.New()
|
||||||
|
g.Use(gin.Recovery())
|
||||||
|
|
||||||
|
pgapi.SetupRoutes(g)
|
||||||
|
pgapi.SetupMetrics(g)
|
||||||
|
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
26
internal/api/mount_dns_routes.go
Normal file
26
internal/api/mount_dns_routes.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountDNSRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/dns", func(d chi.Router) {
|
||||||
|
d.Use(authOrg)
|
||||||
|
|
||||||
|
d.Get("/domains", handlers.ListDomains(db))
|
||||||
|
d.Post("/domains", handlers.CreateDomain(db))
|
||||||
|
d.Get("/domains/{id}", handlers.GetDomain(db))
|
||||||
|
d.Patch("/domains/{id}", handlers.UpdateDomain(db))
|
||||||
|
d.Delete("/domains/{id}", handlers.DeleteDomain(db))
|
||||||
|
|
||||||
|
d.Get("/domains/{domain_id}/records", handlers.ListRecordSets(db))
|
||||||
|
d.Post("/domains/{domain_id}/records", handlers.CreateRecordSet(db))
|
||||||
|
d.Patch("/records/{id}", handlers.UpdateRecordSet(db))
|
||||||
|
d.Delete("/records/{id}", handlers.DeleteRecordSet(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
20
internal/api/mount_label_routes.go
Normal file
20
internal/api/mount_label_routes.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountLabelRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/labels", func(l chi.Router) {
|
||||||
|
l.Use(authOrg)
|
||||||
|
l.Get("/", handlers.ListLabels(db))
|
||||||
|
l.Post("/", handlers.CreateLabel(db))
|
||||||
|
l.Get("/{id}", handlers.GetLabel(db))
|
||||||
|
l.Patch("/{id}", handlers.UpdateLabel(db))
|
||||||
|
l.Delete("/{id}", handlers.DeleteLabel(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
22
internal/api/mount_me_routes.go
Normal file
22
internal/api/mount_me_routes.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountMeRoutes(r chi.Router, db *gorm.DB, authUser func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/me", func(me chi.Router) {
|
||||||
|
me.Use(authUser)
|
||||||
|
|
||||||
|
me.Get("/", handlers.GetMe(db))
|
||||||
|
me.Patch("/", handlers.UpdateMe(db))
|
||||||
|
|
||||||
|
me.Get("/api-keys", handlers.ListUserAPIKeys(db))
|
||||||
|
me.Post("/api-keys", handlers.CreateUserAPIKey(db))
|
||||||
|
me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
13
internal/api/mount_meta_routes.go
Normal file
13
internal/api/mount_meta_routes.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountMetaRoutes(r chi.Router) {
|
||||||
|
// Versioned JWKS for swagger
|
||||||
|
r.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||||
|
r.Get("/healthz", handlers.HealthCheck)
|
||||||
|
r.Get("/version", handlers.Version)
|
||||||
|
}
|
||||||
40
internal/api/mount_node_pool_routes.go
Normal file
40
internal/api/mount_node_pool_routes.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountNodePoolRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/node-pools", func(n chi.Router) {
|
||||||
|
n.Use(authOrg)
|
||||||
|
n.Get("/", handlers.ListNodePools(db))
|
||||||
|
n.Post("/", handlers.CreateNodePool(db))
|
||||||
|
n.Get("/{id}", handlers.GetNodePool(db))
|
||||||
|
n.Patch("/{id}", handlers.UpdateNodePool(db))
|
||||||
|
n.Delete("/{id}", handlers.DeleteNodePool(db))
|
||||||
|
|
||||||
|
// Servers
|
||||||
|
n.Get("/{id}/servers", handlers.ListNodePoolServers(db))
|
||||||
|
n.Post("/{id}/servers", handlers.AttachNodePoolServers(db))
|
||||||
|
n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db))
|
||||||
|
|
||||||
|
// Taints
|
||||||
|
n.Get("/{id}/taints", handlers.ListNodePoolTaints(db))
|
||||||
|
n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db))
|
||||||
|
n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db))
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
n.Get("/{id}/labels", handlers.ListNodePoolLabels(db))
|
||||||
|
n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db))
|
||||||
|
n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db))
|
||||||
|
|
||||||
|
// Annotations
|
||||||
|
n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db))
|
||||||
|
n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db))
|
||||||
|
n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
35
internal/api/mount_org_routes.go
Normal file
35
internal/api/mount_org_routes.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountOrgRoutes(r chi.Router, db *gorm.DB, authUser, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/orgs", func(o chi.Router) {
|
||||||
|
o.Use(authUser)
|
||||||
|
o.Get("/", handlers.ListMyOrgs(db))
|
||||||
|
o.Post("/", handlers.CreateOrg(db))
|
||||||
|
|
||||||
|
o.Group(func(og chi.Router) {
|
||||||
|
og.Use(authOrg)
|
||||||
|
|
||||||
|
og.Get("/{id}", handlers.GetOrg(db))
|
||||||
|
og.Patch("/{id}", handlers.UpdateOrg(db))
|
||||||
|
og.Delete("/{id}", handlers.DeleteOrg(db))
|
||||||
|
|
||||||
|
// members
|
||||||
|
og.Get("/{id}/members", handlers.ListMembers(db))
|
||||||
|
og.Post("/{id}/members", handlers.AddOrUpdateMember(db))
|
||||||
|
og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db))
|
||||||
|
|
||||||
|
// org-scoped key/secret pair
|
||||||
|
og.Get("/{id}/api-keys", handlers.ListOrgKeys(db))
|
||||||
|
og.Post("/{id}/api-keys", handlers.CreateOrgKey(db))
|
||||||
|
og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
24
internal/api/mount_pprof_routes.go
Normal file
24
internal/api/mount_pprof_routes.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
httpPprof "net/http/pprof"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountPprofRoutes(r chi.Router) {
|
||||||
|
r.Route("/debug/pprof", func(pr chi.Router) {
|
||||||
|
pr.Get("/", httpPprof.Index)
|
||||||
|
pr.Get("/cmdline", httpPprof.Cmdline)
|
||||||
|
pr.Get("/profile", httpPprof.Profile)
|
||||||
|
pr.Get("/symbol", httpPprof.Symbol)
|
||||||
|
pr.Get("/trace", httpPprof.Trace)
|
||||||
|
|
||||||
|
pr.Handle("/allocs", httpPprof.Handler("allocs"))
|
||||||
|
pr.Handle("/block", httpPprof.Handler("block"))
|
||||||
|
pr.Handle("/goroutine", httpPprof.Handler("goroutine"))
|
||||||
|
pr.Handle("/heap", httpPprof.Handler("heap"))
|
||||||
|
pr.Handle("/mutex", httpPprof.Handler("mutex"))
|
||||||
|
pr.Handle("/threadcreate", httpPprof.Handler("threadcreate"))
|
||||||
|
})
|
||||||
|
}
|
||||||
21
internal/api/mount_server_routes.go
Normal file
21
internal/api/mount_server_routes.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountServerRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/servers", func(s chi.Router) {
|
||||||
|
s.Use(authOrg)
|
||||||
|
s.Get("/", handlers.ListServers(db))
|
||||||
|
s.Post("/", handlers.CreateServer(db))
|
||||||
|
s.Get("/{id}", handlers.GetServer(db))
|
||||||
|
s.Patch("/{id}", handlers.UpdateServer(db))
|
||||||
|
s.Delete("/{id}", handlers.DeleteServer(db))
|
||||||
|
s.Post("/{id}/reset-hostkey", handlers.ResetServerHostKey(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
20
internal/api/mount_ssh_routes.go
Normal file
20
internal/api/mount_ssh_routes.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountSSHRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/ssh", func(s chi.Router) {
|
||||||
|
s.Use(authOrg)
|
||||||
|
s.Get("/", handlers.ListPublicSshKeys(db))
|
||||||
|
s.Post("/", handlers.CreateSSHKey(db))
|
||||||
|
s.Get("/{id}", handlers.GetSSHKey(db))
|
||||||
|
s.Delete("/{id}", handlers.DeleteSSHKey(db))
|
||||||
|
s.Get("/{id}/download", handlers.DownloadSSHKey(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
15
internal/api/mount_swagger_routes.go
Normal file
15
internal/api/mount_swagger_routes.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/glueops/autoglue/docs"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountSwaggerRoutes(r chi.Router) {
|
||||||
|
r.Get("/swagger/*", httpSwagger.Handler(
|
||||||
|
httpSwagger.URL("swagger.json"),
|
||||||
|
))
|
||||||
|
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||||
|
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
||||||
|
}
|
||||||
20
internal/api/mount_taint_routes.go
Normal file
20
internal/api/mount_taint_routes.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountTaintRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/taints", func(t chi.Router) {
|
||||||
|
t.Use(authOrg)
|
||||||
|
t.Get("/", handlers.ListTaints(db))
|
||||||
|
t.Post("/", handlers.CreateTaint(db))
|
||||||
|
t.Get("/{id}", handlers.GetTaint(db))
|
||||||
|
t.Patch("/{id}", handlers.UpdateTaint(db))
|
||||||
|
t.Delete("/{id}", handlers.DeleteTaint(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
|||||||
// Google font files
|
// Google font files
|
||||||
"font-src 'self' data: https://fonts.gstatic.com",
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
// HMR connections
|
// HMR connections
|
||||||
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080",
|
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080 https://api.github.com",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
}, "; "))
|
}, "; "))
|
||||||
} else {
|
} else {
|
||||||
@@ -53,7 +53,7 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
|||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
"font-src 'self' data: https://fonts.gstatic.com",
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
"connect-src 'self'",
|
"connect-src 'self' ws://localhost:8080 https://api.github.com",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
}, "; "))
|
}, "; "))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
httpPprof "net/http/pprof"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/glueops/autoglue/docs"
|
|
||||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
"github.com/glueops/autoglue/internal/bg"
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
"github.com/glueops/autoglue/internal/config"
|
"github.com/glueops/autoglue/internal/config"
|
||||||
@@ -23,8 +22,6 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
||||||
@@ -38,7 +35,6 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
|||||||
r.Use(middleware.RealIP)
|
r.Use(middleware.RealIP)
|
||||||
r.Use(zeroLogMiddleware())
|
r.Use(zeroLogMiddleware())
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
// r.Use(middleware.RedirectSlashes)
|
|
||||||
r.Use(SecurityHeaders)
|
r.Use(SecurityHeaders)
|
||||||
r.Use(requestBodyLimit(10 << 20))
|
r.Use(requestBodyLimit(10 << 20))
|
||||||
r.Use(httprate.LimitByIP(100, 1*time.Minute))
|
r.Use(httprate.LimitByIP(100, 1*time.Minute))
|
||||||
@@ -60,198 +56,44 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
|||||||
MaxAge: 600,
|
MaxAge: 600,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
r.Use(middleware.AllowContentType("application/json"))
|
r.Use(middleware.Maybe(
|
||||||
|
middleware.AllowContentType("application/json"),
|
||||||
|
func(r *http.Request) bool {
|
||||||
|
// return true => run AllowContentType
|
||||||
|
// return false => skip AllowContentType for this request
|
||||||
|
return !strings.HasPrefix(r.URL.Path, "/db-studio")
|
||||||
|
}))
|
||||||
|
//r.Use(middleware.AllowContentType("application/json"))
|
||||||
|
|
||||||
|
// Unversioned, non-auth endpoints
|
||||||
r.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
r.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||||
r.Route("/api", func(api chi.Router) {
|
|
||||||
api.Route("/v1", func(v1 chi.Router) {
|
|
||||||
authUser := httpmiddleware.AuthMiddleware(db, false)
|
|
||||||
authOrg := httpmiddleware.AuthMiddleware(db, true)
|
|
||||||
|
|
||||||
// Also serving a versioned JWKS for swagger, which uses BasePath
|
// Versioned API
|
||||||
v1.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
mountAPIRoutes(r, db, jobs)
|
||||||
|
|
||||||
v1.Get("/healthz", handlers.HealthCheck)
|
|
||||||
v1.Get("/version", handlers.Version)
|
|
||||||
|
|
||||||
v1.Route("/auth", func(a chi.Router) {
|
|
||||||
a.Post("/{provider}/start", handlers.AuthStart(db))
|
|
||||||
a.Get("/{provider}/callback", handlers.AuthCallback(db))
|
|
||||||
a.Post("/refresh", handlers.Refresh(db))
|
|
||||||
a.Post("/logout", handlers.Logout(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/admin", func(admin chi.Router) {
|
|
||||||
admin.Route("/archer", func(archer chi.Router) {
|
|
||||||
archer.Use(authUser)
|
|
||||||
archer.Use(httpmiddleware.RequirePlatformAdmin())
|
|
||||||
|
|
||||||
archer.Get("/jobs", handlers.AdminListArcherJobs(db))
|
|
||||||
archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs))
|
|
||||||
archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db))
|
|
||||||
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
|
||||||
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/me", func(me chi.Router) {
|
|
||||||
me.Use(authUser)
|
|
||||||
|
|
||||||
me.Get("/", handlers.GetMe(db))
|
|
||||||
me.Patch("/", handlers.UpdateMe(db))
|
|
||||||
|
|
||||||
me.Get("/api-keys", handlers.ListUserAPIKeys(db))
|
|
||||||
me.Post("/api-keys", handlers.CreateUserAPIKey(db))
|
|
||||||
me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/orgs", func(o chi.Router) {
|
|
||||||
o.Use(authUser)
|
|
||||||
o.Get("/", handlers.ListMyOrgs(db))
|
|
||||||
o.Post("/", handlers.CreateOrg(db))
|
|
||||||
|
|
||||||
o.Group(func(og chi.Router) {
|
|
||||||
og.Use(authOrg)
|
|
||||||
og.Get("/{id}", handlers.GetOrg(db))
|
|
||||||
og.Patch("/{id}", handlers.UpdateOrg(db))
|
|
||||||
og.Delete("/{id}", handlers.DeleteOrg(db))
|
|
||||||
|
|
||||||
// members
|
|
||||||
og.Get("/{id}/members", handlers.ListMembers(db))
|
|
||||||
og.Post("/{id}/members", handlers.AddOrUpdateMember(db))
|
|
||||||
og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db))
|
|
||||||
|
|
||||||
// org-scoped key/secret pair
|
|
||||||
og.Get("/{id}/api-keys", handlers.ListOrgKeys(db))
|
|
||||||
og.Post("/{id}/api-keys", handlers.CreateOrgKey(db))
|
|
||||||
og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/credentials", func(c chi.Router) {
|
|
||||||
c.Use(authOrg)
|
|
||||||
c.Get("/", handlers.ListCredentials(db))
|
|
||||||
c.Post("/", handlers.CreateCredential(db))
|
|
||||||
c.Get("/{id}", handlers.GetCredential(db))
|
|
||||||
c.Patch("/{id}", handlers.UpdateCredential(db))
|
|
||||||
c.Delete("/{id}", handlers.DeleteCredential(db))
|
|
||||||
c.Post("/{id}/reveal", handlers.RevealCredential(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/ssh", func(s chi.Router) {
|
|
||||||
s.Use(authOrg)
|
|
||||||
s.Get("/", handlers.ListPublicSshKeys(db))
|
|
||||||
s.Post("/", handlers.CreateSSHKey(db))
|
|
||||||
s.Get("/{id}", handlers.GetSSHKey(db))
|
|
||||||
s.Delete("/{id}", handlers.DeleteSSHKey(db))
|
|
||||||
s.Get("/{id}/download", handlers.DownloadSSHKey(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/servers", func(s chi.Router) {
|
|
||||||
s.Use(authOrg)
|
|
||||||
s.Get("/", handlers.ListServers(db))
|
|
||||||
s.Post("/", handlers.CreateServer(db))
|
|
||||||
s.Get("/{id}", handlers.GetServer(db))
|
|
||||||
s.Patch("/{id}", handlers.UpdateServer(db))
|
|
||||||
s.Delete("/{id}", handlers.DeleteServer(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/taints", func(s chi.Router) {
|
|
||||||
s.Use(authOrg)
|
|
||||||
s.Get("/", handlers.ListTaints(db))
|
|
||||||
s.Post("/", handlers.CreateTaint(db))
|
|
||||||
s.Get("/{id}", handlers.GetTaint(db))
|
|
||||||
s.Patch("/{id}", handlers.UpdateTaint(db))
|
|
||||||
s.Delete("/{id}", handlers.DeleteTaint(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/labels", func(l chi.Router) {
|
|
||||||
l.Use(authOrg)
|
|
||||||
l.Get("/", handlers.ListLabels(db))
|
|
||||||
l.Post("/", handlers.CreateLabel(db))
|
|
||||||
l.Get("/{id}", handlers.GetLabel(db))
|
|
||||||
l.Patch("/{id}", handlers.UpdateLabel(db))
|
|
||||||
l.Delete("/{id}", handlers.DeleteLabel(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/annotations", func(a chi.Router) {
|
|
||||||
a.Use(authOrg)
|
|
||||||
a.Get("/", handlers.ListAnnotations(db))
|
|
||||||
a.Post("/", handlers.CreateAnnotation(db))
|
|
||||||
a.Get("/{id}", handlers.GetAnnotation(db))
|
|
||||||
a.Patch("/{id}", handlers.UpdateAnnotation(db))
|
|
||||||
a.Delete("/{id}", handlers.DeleteAnnotation(db))
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Route("/node-pools", func(n chi.Router) {
|
|
||||||
n.Use(authOrg)
|
|
||||||
n.Get("/", handlers.ListNodePools(db))
|
|
||||||
n.Post("/", handlers.CreateNodePool(db))
|
|
||||||
n.Get("/{id}", handlers.GetNodePool(db))
|
|
||||||
n.Patch("/{id}", handlers.UpdateNodePool(db))
|
|
||||||
n.Delete("/{id}", handlers.DeleteNodePool(db))
|
|
||||||
|
|
||||||
// Servers
|
|
||||||
n.Get("/{id}/servers", handlers.ListNodePoolServers(db))
|
|
||||||
n.Post("/{id}/servers", handlers.AttachNodePoolServers(db))
|
|
||||||
n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db))
|
|
||||||
|
|
||||||
// Taints
|
|
||||||
n.Get("/{id}/taints", handlers.ListNodePoolTaints(db))
|
|
||||||
n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db))
|
|
||||||
n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db))
|
|
||||||
|
|
||||||
// Labels
|
|
||||||
n.Get("/{id}/labels", handlers.ListNodePoolLabels(db))
|
|
||||||
n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db))
|
|
||||||
n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db))
|
|
||||||
|
|
||||||
// Annotations
|
|
||||||
n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db))
|
|
||||||
n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db))
|
|
||||||
n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// Optional DB studio
|
||||||
if studio != nil {
|
if studio != nil {
|
||||||
r.Group(func(gr chi.Router) {
|
r.Group(func(gr chi.Router) {
|
||||||
authUser := httpmiddleware.AuthMiddleware(db, false)
|
authUser := httpmiddleware.AuthMiddleware(db, false)
|
||||||
adminOnly := httpmiddleware.RequirePlatformAdmin()
|
adminOnly := httpmiddleware.RequirePlatformAdmin()
|
||||||
gr.Use(authUser)
|
gr.Use(authUser, adminOnly)
|
||||||
gr.Use(adminOnly)
|
|
||||||
gr.Mount("/db-studio", studio)
|
gr.Mount("/db-studio", studio)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pprof
|
||||||
if config.IsDebug() {
|
if config.IsDebug() {
|
||||||
r.Route("/debug/pprof", func(pr chi.Router) {
|
mountPprofRoutes(r)
|
||||||
pr.Get("/", httpPprof.Index)
|
|
||||||
pr.Get("/cmdline", httpPprof.Cmdline)
|
|
||||||
pr.Get("/profile", httpPprof.Profile)
|
|
||||||
pr.Get("/symbol", httpPprof.Symbol)
|
|
||||||
pr.Get("/trace", httpPprof.Trace)
|
|
||||||
|
|
||||||
pr.Handle("/allocs", httpPprof.Handler("allocs"))
|
|
||||||
pr.Handle("/block", httpPprof.Handler("block"))
|
|
||||||
pr.Handle("/goroutine", httpPprof.Handler("goroutine"))
|
|
||||||
pr.Handle("/heap", httpPprof.Handler("heap"))
|
|
||||||
pr.Handle("/mutex", httpPprof.Handler("mutex"))
|
|
||||||
pr.Handle("/threadcreate", httpPprof.Handler("threadcreate"))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swagger
|
||||||
if config.IsSwaggerEnabled() {
|
if config.IsSwaggerEnabled() {
|
||||||
r.Get("/swagger/*", httpSwagger.Handler(
|
mountSwaggerRoutes(r)
|
||||||
httpSwagger.URL("swagger.json"),
|
|
||||||
))
|
|
||||||
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
|
||||||
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UI dev/prod
|
||||||
if config.IsUIDev() {
|
if config.IsUIDev() {
|
||||||
fmt.Println("Running in development mode")
|
fmt.Println("Running in development mode")
|
||||||
// Dev: isolate proxy from chi middlewares so WS upgrade can hijack.
|
|
||||||
proxy, err := web.DevProxy("http://localhost:5173")
|
proxy, err := web.DevProxy("http://localhost:5173")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("dev proxy init failed")
|
log.Error().Err(err).Msg("dev proxy init failed")
|
||||||
@@ -259,23 +101,20 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
// Send API/Swagger/pprof to chi
|
|
||||||
mux.Handle("/api/", r)
|
mux.Handle("/api/", r)
|
||||||
mux.Handle("/api", r)
|
mux.Handle("/api", r)
|
||||||
mux.Handle("/swagger/", r)
|
mux.Handle("/swagger/", r)
|
||||||
mux.Handle("/db-studio/", r)
|
mux.Handle("/db-studio/", r)
|
||||||
mux.Handle("/debug/pprof/", r)
|
mux.Handle("/debug/pprof/", r)
|
||||||
// Everything else (/, /brand-preview, assets) → proxy (no middlewares)
|
|
||||||
mux.Handle("/", proxy)
|
mux.Handle("/", proxy)
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Running in production mode")
|
||||||
|
if h, err := web.SPAHandler(); err == nil {
|
||||||
|
r.NotFound(h.ServeHTTP)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Running in production mode")
|
log.Error().Err(err).Msg("spa handler init failed")
|
||||||
if h, err := web.SPAHandler(); err == nil {
|
|
||||||
r.NotFound(h.ServeHTTP)
|
|
||||||
} else {
|
|
||||||
log.Error().Err(err).Msg("spa handler init failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ func NewRuntime() *Runtime {
|
|||||||
&models.NodePool{},
|
&models.NodePool{},
|
||||||
&models.Cluster{},
|
&models.Cluster{},
|
||||||
&models.Credential{},
|
&models.Credential{},
|
||||||
|
&models.Domain{},
|
||||||
|
&models.RecordSet{},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbBackupArgs struct {
|
type DbBackupArgs struct {
|
||||||
// kept in case you want to change retention or add dry-run later
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type s3Scope struct {
|
type s3Scope struct {
|
||||||
@@ -44,6 +44,13 @@ type encAWS struct {
|
|||||||
|
|
||||||
func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
return func(ctx context.Context, j job.Job) (any, error) {
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := DbBackupArgs{IntervalS: 3600}
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 3600
|
||||||
|
}
|
||||||
|
|
||||||
if err := DbBackup(ctx, db); err != nil {
|
if err := DbBackup(ctx, db); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -53,7 +60,7 @@ func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
|||||||
queue = "db_backup_s3"
|
queue = "db_backup_s3"
|
||||||
}
|
}
|
||||||
|
|
||||||
next := time.Now().UTC().Add(1 * time.Hour)
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
|
||||||
payload := DbBackupArgs{}
|
payload := DbBackupArgs{}
|
||||||
|
|
||||||
@@ -73,7 +80,6 @@ func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
|||||||
|
|
||||||
func DbBackup(ctx context.Context, db *gorm.DB) error {
|
func DbBackup(ctx context.Context, db *gorm.DB) error {
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
log.Info().Err(err).Msg("loading config")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load config: %w", err)
|
return fmt.Errorf("load config: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package bg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,13 +13,16 @@ import (
|
|||||||
"github.com/glueops/autoglue/internal/models"
|
"github.com/glueops/autoglue/internal/models"
|
||||||
"github.com/glueops/autoglue/internal/utils"
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ----- Public types -----
|
// ----- Public types -----
|
||||||
|
|
||||||
type BastionBootstrapArgs struct{}
|
type BastionBootstrapArgs struct {
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type BastionBootstrapFailure struct {
|
type BastionBootstrapFailure struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
@@ -39,11 +42,17 @@ type BastionBootstrapResult struct {
|
|||||||
|
|
||||||
// ----- Worker -----
|
// ----- Worker -----
|
||||||
|
|
||||||
func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
func BastionBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
return func(ctx context.Context, j job.Job) (any, error) {
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := BastionBootstrapArgs{IntervalS: 120}
|
||||||
jobID := j.ID
|
jobID := j.ID
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 120
|
||||||
|
}
|
||||||
|
|
||||||
var servers []models.Server
|
var servers []models.Server
|
||||||
if err := db.
|
if err := db.
|
||||||
Preload("SshKey").
|
Preload("SshKey").
|
||||||
@@ -105,7 +114,7 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
|||||||
// 4) SSH + install docker
|
// 4) SSH + install docker
|
||||||
host := net.JoinHostPort(*s.PublicIPAddress, "22")
|
host := net.JoinHostPort(*s.PublicIPAddress, "22")
|
||||||
runCtx, cancel := context.WithTimeout(ctx, perHostTimeout)
|
runCtx, cancel := context.WithTimeout(ctx, perHostTimeout)
|
||||||
out, err := sshInstallDockerWithOutput(runCtx, host, s.SSHUser, []byte(privKey))
|
out, err := sshInstallDockerWithOutput(runCtx, db, s, host, s.SSHUser, []byte(privKey))
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,9 +156,17 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
|||||||
Failures: failures,
|
Failures: failures,
|
||||||
}
|
}
|
||||||
|
|
||||||
// log.Printf("[bastion] level=INFO job=%s step=finish processed=%d ready=%d failed=%d elapsed_ms=%d",
|
log.Debug().Int("processed", proc).Int("ready", ok).Int("failed", fail).Msg("[bastion] reconcile tick ok")
|
||||||
// jobID, proc, ok, fail, res.ElapsedMs)
|
|
||||||
|
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"bootstrap_bastion",
|
||||||
|
args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,16 +204,24 @@ func logHostInfo(jobID string, s *models.Server, step, msg string, kv ...any) {
|
|||||||
// ----- SSH & command execution -----
|
// ----- SSH & command execution -----
|
||||||
|
|
||||||
// returns combined stdout/stderr so caller can log it on error
|
// returns combined stdout/stderr so caller can log it on error
|
||||||
func sshInstallDockerWithOutput(ctx context.Context, host, user string, privateKeyPEM []byte) (string, error) {
|
func sshInstallDockerWithOutput(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
s *models.Server,
|
||||||
|
host, user string,
|
||||||
|
privateKeyPEM []byte,
|
||||||
|
) (string, error) {
|
||||||
signer, err := ssh.ParsePrivateKey(privateKeyPEM)
|
signer, err := ssh.ParsePrivateKey(privateKeyPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("parse private key: %w", err)
|
return "", fmt.Errorf("parse private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hkcb := makeDBHostKeyCallback(db, s)
|
||||||
|
|
||||||
config := &ssh.ClientConfig{
|
config := &ssh.ClientConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: known_hosts verification
|
HostKeyCallback: hkcb,
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,3 +519,38 @@ func wrapSSHError(err error, output string) error {
|
|||||||
func sshEscape(s string) string {
|
func sshEscape(s string) string {
|
||||||
return fmt.Sprintf("%q", s)
|
return fmt.Sprintf("%q", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeDBHostKeyCallback returns a HostKeyCallback bound to a specific server row.
|
||||||
|
// TOFU semantics:
|
||||||
|
// - If s.SSHHostKey is empty: store the current key in DB and accept.
|
||||||
|
// - If s.SSHHostKey is set: require exact match, else error (possible MITM/reinstall).
|
||||||
|
func makeDBHostKeyCallback(db *gorm.DB, s *models.Server) ssh.HostKeyCallback {
|
||||||
|
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||||
|
algo := key.Type()
|
||||||
|
enc := base64.StdEncoding.EncodeToString(key.Marshal())
|
||||||
|
|
||||||
|
// First-time connect: persist key (TOFU).
|
||||||
|
if s.SSHHostKey == "" {
|
||||||
|
if err := db.Model(&models.Server{}).
|
||||||
|
Where("id = ? AND (ssh_host_key IS NULL or ssh_host_key = '')", s.ID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"ssh_host_key": enc,
|
||||||
|
"ssh_host_key_algo": algo,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fmt.Errorf("store new host key for %s (%s): %w", hostname, s.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SSHHostKey = enc
|
||||||
|
s.SSHHostKeyAlgo = algo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.SSHHostKeyAlgo != algo || s.SSHHostKey != enc {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"host key mismatch for %s (server_id=%s, stored=%s/%s, got=%s/%s) - POSSIBLE MITM or host reinstalled",
|
||||||
|
hostname, s.ID, s.SSHHostKeyAlgo, s.SSHHostKey, algo, enc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
|||||||
|
|
||||||
c.Register(
|
c.Register(
|
||||||
"bootstrap_bastion",
|
"bootstrap_bastion",
|
||||||
BastionBootstrapWorker(gdb),
|
BastionBootstrapWorker(gdb, jobs),
|
||||||
archer.WithInstances(instances),
|
archer.WithInstances(instances),
|
||||||
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
||||||
)
|
)
|
||||||
@@ -100,6 +100,13 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
|||||||
archer.WithInstances(1),
|
archer.WithInstances(1),
|
||||||
archer.WithTimeout(15*time.Minute),
|
archer.WithTimeout(15*time.Minute),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
c.Register(
|
||||||
|
"dns_reconcile",
|
||||||
|
DNSReconsileWorker(gdb, jobs),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(2*time.Minute),
|
||||||
|
)
|
||||||
return jobs, nil
|
return jobs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
597
internal/bg/dns.go
Normal file
597
internal/bg/dns.go
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
r53 "github.com/aws/aws-sdk-go-v2/service/route53"
|
||||||
|
r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
/************* args & small DTOs *************/
|
||||||
|
|
||||||
|
type DNSReconcileArgs struct {
|
||||||
|
MaxDomains int `json:"max_domains,omitempty"`
|
||||||
|
MaxRecords int `json:"max_records,omitempty"`
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TXT marker content (compact)
|
||||||
|
type ownershipMarker struct {
|
||||||
|
Ver string `json:"v"` // "ag1"
|
||||||
|
Org string `json:"org"` // org UUID
|
||||||
|
Rec string `json:"rec"` // record UUID
|
||||||
|
Fp string `json:"fp"` // short fp (first 16 of sha256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalDNS poison owner id – MUST NOT match any real external-dns --txt-owner-id
|
||||||
|
const externalDNSPoisonOwner = "autoglue-lock"
|
||||||
|
|
||||||
|
// ExternalDNS poison content – fake owner so real external-dns skips it.
|
||||||
|
const externalDNSPoisonValue = "heritage=external-dns,external-dns/owner=" + externalDNSPoisonOwner + ",external-dns/resource=manual/autoglue"
|
||||||
|
|
||||||
|
/************* entrypoint worker *************/
|
||||||
|
|
||||||
|
func DNSReconsileWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := DNSReconcileArgs{MaxDomains: 25, MaxRecords: 100, IntervalS: 30}
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
|
||||||
|
if args.MaxDomains <= 0 {
|
||||||
|
args.MaxDomains = 25
|
||||||
|
}
|
||||||
|
if args.MaxRecords <= 0 {
|
||||||
|
args.MaxRecords = 100
|
||||||
|
}
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
processedDomains, processedRecords, err := reconcileDNSOnce(ctx, db, args)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[dns] reconcile tick failed")
|
||||||
|
} else {
|
||||||
|
log.Debug().
|
||||||
|
Int("domains", processedDomains).
|
||||||
|
Int("records", processedRecords).
|
||||||
|
Msg("[dns] reconcile tick ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(ctx, uuid.NewString(), "dns_reconcile", args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"domains_processed": processedDomains,
|
||||||
|
"records_processed": processedRecords,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/************* core tick *************/
|
||||||
|
|
||||||
|
func reconcileDNSOnce(ctx context.Context, db *gorm.DB, args DNSReconcileArgs) (int, int, error) {
|
||||||
|
var domains []models.Domain
|
||||||
|
|
||||||
|
// 1) validate/backfill pending domains
|
||||||
|
if err := db.
|
||||||
|
Where("status = ?", "pending").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Limit(args.MaxDomains).
|
||||||
|
Find(&domains).Error; err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domainsProcessed := 0
|
||||||
|
for i := range domains {
|
||||||
|
if err := processDomain(ctx, db, &domains[i]); err != nil {
|
||||||
|
log.Error().Err(err).Str("domain", domains[i].DomainName).Msg("[dns] domain processing failed")
|
||||||
|
} else {
|
||||||
|
domainsProcessed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) apply pending record sets for ready domains
|
||||||
|
var readyDomains []models.Domain
|
||||||
|
if err := db.Where("status = ?", "ready").Find(&readyDomains).Error; err != nil {
|
||||||
|
return domainsProcessed, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recordsProcessed := 0
|
||||||
|
for i := range readyDomains {
|
||||||
|
n, err := processPendingRecordsForDomain(ctx, db, &readyDomains[i], args.MaxRecords)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("domain", readyDomains[i].DomainName).Msg("[dns] record processing failed")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
recordsProcessed += n
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainsProcessed, recordsProcessed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/************* domain processing *************/
|
||||||
|
|
||||||
|
func processDomain(ctx context.Context, db *gorm.DB, d *models.Domain) error {
|
||||||
|
orgID := d.OrganizationID
|
||||||
|
|
||||||
|
// 1) Load credential (org-guarded)
|
||||||
|
var cred models.Credential
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", d.CredentialID, orgID).First(&cred).Error; err != nil {
|
||||||
|
return setDomainFailed(db, d, fmt.Errorf("credential not found: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Decrypt → dto.AWSCredential
|
||||||
|
secret, err := utils.DecryptForOrg(orgID, cred.EncryptedData, cred.IV, cred.Tag, db)
|
||||||
|
if err != nil {
|
||||||
|
return setDomainFailed(db, d, fmt.Errorf("decrypt: %w", err))
|
||||||
|
}
|
||||||
|
var awsCred dto.AWSCredential
|
||||||
|
if err := jsonUnmarshalStrict([]byte(secret), &awsCred); err != nil {
|
||||||
|
return setDomainFailed(db, d, fmt.Errorf("secret decode: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Client
|
||||||
|
r53c, _, err := newRoute53Client(ctx, awsCred)
|
||||||
|
if err != nil {
|
||||||
|
return setDomainFailed(db, d, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Backfill zone id if missing
|
||||||
|
zoneID := strings.TrimSpace(d.ZoneID)
|
||||||
|
if zoneID == "" {
|
||||||
|
zid, err := findHostedZoneID(ctx, r53c, d.DomainName)
|
||||||
|
if err != nil {
|
||||||
|
return setDomainFailed(db, d, fmt.Errorf("discover zone id: %w", err))
|
||||||
|
}
|
||||||
|
zoneID = zid
|
||||||
|
d.ZoneID = zoneID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Sanity: can fetch zone
|
||||||
|
if _, err := r53c.GetHostedZone(ctx, &r53.GetHostedZoneInput{Id: aws.String(zoneID)}); err != nil {
|
||||||
|
return setDomainFailed(db, d, fmt.Errorf("get hosted zone: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Mark ready
|
||||||
|
d.Status = "ready"
|
||||||
|
d.LastError = ""
|
||||||
|
if err := db.Save(d).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDomainFailed(db *gorm.DB, d *models.Domain, cause error) error {
|
||||||
|
d.Status = "failed"
|
||||||
|
d.LastError = truncateErr(cause.Error())
|
||||||
|
_ = db.Save(d).Error
|
||||||
|
return cause
|
||||||
|
}
|
||||||
|
|
||||||
|
/************* record processing *************/
|
||||||
|
|
||||||
|
func processPendingRecordsForDomain(ctx context.Context, db *gorm.DB, d *models.Domain, max int) (int, error) {
|
||||||
|
orgID := d.OrganizationID
|
||||||
|
|
||||||
|
// reload credential
|
||||||
|
var cred models.Credential
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", d.CredentialID, orgID).First(&cred).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := utils.DecryptForOrg(orgID, cred.EncryptedData, cred.IV, cred.Tag, db)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var awsCred dto.AWSCredential
|
||||||
|
if err := jsonUnmarshalStrict([]byte(secret), &awsCred); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r53c, _, err := newRoute53Client(ctx, awsCred)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []models.RecordSet
|
||||||
|
if err := db.
|
||||||
|
Where("domain_id = ? AND status = ?", d.ID, "pending").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Limit(max).
|
||||||
|
Find(&records).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
applied := 0
|
||||||
|
for i := range records {
|
||||||
|
if err := applyRecord(ctx, db, r53c, d, &records[i]); err != nil {
|
||||||
|
log.Error().Err(err).Str("rr", records[i].Name).Msg("[dns] apply record failed")
|
||||||
|
_ = setRecordFailed(db, &records[i], err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
applied++
|
||||||
|
}
|
||||||
|
return applied, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// core write + ownership + external-dns hardening
|
||||||
|
|
||||||
|
func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.Domain, r *models.RecordSet) error {
|
||||||
|
zoneID := strings.TrimSpace(d.ZoneID)
|
||||||
|
if zoneID == "" {
|
||||||
|
return errors.New("domain has no zone_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := strings.ToUpper(r.Type)
|
||||||
|
|
||||||
|
// FQDN & marker
|
||||||
|
fq := recordFQDN(r.Name, d.DomainName) // ends with "."
|
||||||
|
mname := markerName(fq)
|
||||||
|
expected := buildMarkerValue(d.OrganizationID.String(), r.ID.String(), r.Fingerprint)
|
||||||
|
|
||||||
|
// ---- ExternalDNS preflight ----
|
||||||
|
extOwned, err := hasExternalDNSOwnership(ctx, r53c, zoneID, fq, rt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("external_dns_lookup: %w", err)
|
||||||
|
}
|
||||||
|
if extOwned {
|
||||||
|
r.Owner = "external"
|
||||||
|
_ = db.Save(r).Error
|
||||||
|
return fmt.Errorf("ownership_conflict: external-dns claims %s; refusing to modify", strings.TrimSuffix(fq, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Autoglue ownership preflight via _autoglue.<fqdn> TXT ----
|
||||||
|
markerVals, err := getMarkerTXTValues(ctx, r53c, zoneID, mname)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marker lookup: %w", err)
|
||||||
|
}
|
||||||
|
hasForeignOwner := false
|
||||||
|
hasOurExact := false
|
||||||
|
for _, v := range markerVals {
|
||||||
|
mk, ok := parseMarkerValue(v)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case mk.Org == d.OrganizationID.String() && mk.Rec == r.ID.String() && mk.Fp == shortFP(r.Fingerprint):
|
||||||
|
hasOurExact = true
|
||||||
|
case mk.Org != d.OrganizationID.String() || mk.Rec != r.ID.String():
|
||||||
|
hasForeignOwner = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasForeignOwner {
|
||||||
|
r.Owner = "external"
|
||||||
|
_ = db.Save(r).Error
|
||||||
|
return fmt.Errorf("ownership_conflict: marker for %s is owned by another controller; refusing to modify", strings.TrimSuffix(fq, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build RR change (UPSERT)
|
||||||
|
rrChange := r53types.Change{
|
||||||
|
Action: r53types.ChangeActionUpsert,
|
||||||
|
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||||
|
Name: aws.String(fq),
|
||||||
|
Type: r53types.RRType(rt),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode user values
|
||||||
|
var userVals []string
|
||||||
|
if len(r.Values) > 0 {
|
||||||
|
if err := jsonUnmarshalStrict([]byte(r.Values), &userVals); err != nil {
|
||||||
|
return fmt.Errorf("values decode: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote TXT values as required by Route53
|
||||||
|
recs := make([]r53types.ResourceRecord, 0, len(userVals))
|
||||||
|
for _, v := range userVals {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if rt == "TXT" && !(strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`)) {
|
||||||
|
v = strconv.Quote(v)
|
||||||
|
}
|
||||||
|
recs = append(recs, r53types.ResourceRecord{Value: aws.String(v)})
|
||||||
|
}
|
||||||
|
rrChange.ResourceRecordSet.ResourceRecords = recs
|
||||||
|
if r.TTL != nil {
|
||||||
|
ttl := int64(*r.TTL)
|
||||||
|
rrChange.ResourceRecordSet.TTL = aws.Int64(ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build marker TXT change (UPSERT)
|
||||||
|
markerChange := r53types.Change{
|
||||||
|
Action: r53types.ChangeActionUpsert,
|
||||||
|
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||||
|
Name: aws.String(mname),
|
||||||
|
Type: r53types.RRTypeTxt,
|
||||||
|
TTL: aws.Int64(300),
|
||||||
|
ResourceRecords: []r53types.ResourceRecord{
|
||||||
|
{Value: aws.String(strconv.Quote(expected))},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build external-dns poison TXT changes
|
||||||
|
poisonChanges := buildExternalDNSPoisonTXTChanges(fq, rt)
|
||||||
|
|
||||||
|
// Apply all in one batch (atomic-ish)
|
||||||
|
changes := []r53types.Change{rrChange, markerChange}
|
||||||
|
changes = append(changes, poisonChanges...)
|
||||||
|
|
||||||
|
_, err = r53c.ChangeResourceRecordSets(ctx, &r53.ChangeResourceRecordSetsInput{
|
||||||
|
HostedZoneId: aws.String(zoneID),
|
||||||
|
ChangeBatch: &r53types.ChangeBatch{Changes: changes},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success → mark ready & ownership
|
||||||
|
r.Status = "ready"
|
||||||
|
r.LastError = ""
|
||||||
|
r.Owner = "autoglue"
|
||||||
|
if err := db.Save(r).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = hasOurExact // could be used to skip marker write in future
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRecordFailed(db *gorm.DB, r *models.RecordSet, cause error) error {
|
||||||
|
msg := truncateErr(cause.Error())
|
||||||
|
r.Status = "failed"
|
||||||
|
r.LastError = msg
|
||||||
|
// classify ownership on conflict
|
||||||
|
if strings.HasPrefix(msg, "ownership_conflict") {
|
||||||
|
r.Owner = "external"
|
||||||
|
} else if r.Owner == "" || r.Owner == "unknown" {
|
||||||
|
r.Owner = "unknown"
|
||||||
|
}
|
||||||
|
_ = db.Save(r).Error
|
||||||
|
return cause
|
||||||
|
}
|
||||||
|
|
||||||
|
/************* AWS helpers *************/
|
||||||
|
|
||||||
|
func newRoute53Client(ctx context.Context, cred dto.AWSCredential) (*r53.Client, *aws.Config, error) {
|
||||||
|
// Route53 is global, but config still wants a region
|
||||||
|
region := strings.TrimSpace(cred.Region)
|
||||||
|
if region == "" {
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
cfg, err := config.LoadDefaultConfig(ctx,
|
||||||
|
config.WithRegion(region),
|
||||||
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||||
|
cred.AccessKeyID, cred.SecretAccessKey, "",
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return r53.NewFromConfig(cfg), &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHostedZoneID(ctx context.Context, c *r53.Client, domain string) (string, error) {
|
||||||
|
d := normalizeDomain(domain)
|
||||||
|
out, err := c.ListHostedZonesByName(ctx, &r53.ListHostedZonesByNameInput{
|
||||||
|
DNSName: aws.String(d),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, hz := range out.HostedZones {
|
||||||
|
if strings.TrimSuffix(aws.ToString(hz.Name), ".") == d {
|
||||||
|
return trimZoneID(aws.ToString(hz.Id)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("hosted zone not found for %q", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimZoneID(id string) string {
|
||||||
|
return strings.TrimPrefix(id, "/hostedzone/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDomain(s string) string {
|
||||||
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
|
return strings.TrimSuffix(s, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordFQDN(name, domain string) string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" || name == "@" {
|
||||||
|
return normalizeDomain(domain) + "."
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(name, ".") {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s.%s.", name, normalizeDomain(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
/************* TXT marker / external-dns helpers *************/
|
||||||
|
|
||||||
|
func markerName(fqdn string) string {
|
||||||
|
trimmed := strings.TrimSuffix(fqdn, ".")
|
||||||
|
return "_autoglue." + trimmed + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortFP(full string) string {
|
||||||
|
if len(full) > 16 {
|
||||||
|
return full[:16]
|
||||||
|
}
|
||||||
|
return full
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMarkerValue(orgID, recID, fp string) string {
|
||||||
|
return "v=ag1 org=" + orgID + " rec=" + recID + " fp=" + shortFP(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMarkerValue(s string) (ownershipMarker, bool) {
|
||||||
|
out := ownershipMarker{}
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
return out, false
|
||||||
|
}
|
||||||
|
kv := map[string]string{}
|
||||||
|
for _, f := range fields {
|
||||||
|
parts := strings.SplitN(f, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
kv[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kv["v"] == "" || kv["org"] == "" || kv["rec"] == "" || kv["fp"] == "" {
|
||||||
|
return out, false
|
||||||
|
}
|
||||||
|
out.Ver, out.Org, out.Rec, out.Fp = kv["v"], kv["org"], kv["rec"], kv["fp"]
|
||||||
|
return out, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMarkerTXTValues(ctx context.Context, c *r53.Client, zoneID, marker string) ([]string, error) {
|
||||||
|
return getTXTValues(ctx, c, zoneID, marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic TXT fetcher
|
||||||
|
func getTXTValues(ctx context.Context, c *r53.Client, zoneID, name string) ([]string, error) {
|
||||||
|
out, err := c.ListResourceRecordSets(ctx, &r53.ListResourceRecordSetsInput{
|
||||||
|
HostedZoneId: aws.String(zoneID),
|
||||||
|
StartRecordName: aws.String(name),
|
||||||
|
StartRecordType: r53types.RRTypeTxt,
|
||||||
|
MaxItems: aws.Int32(1),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(out.ResourceRecordSets) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
rrset := out.ResourceRecordSets[0]
|
||||||
|
if aws.ToString(rrset.Name) != name || rrset.Type != r53types.RRTypeTxt {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
vals := make([]string, 0, len(rrset.ResourceRecords))
|
||||||
|
for _, rr := range rrset.ResourceRecords {
|
||||||
|
vals = append(vals, aws.ToString(rr.Value))
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect external-dns-style ownership for this fqdn/type
|
||||||
|
func hasExternalDNSOwnership(ctx context.Context, c *r53.Client, zoneID, fqdn, rrType string) (bool, error) {
|
||||||
|
base := strings.TrimSuffix(fqdn, ".")
|
||||||
|
candidates := []string{
|
||||||
|
// with txtPrefix=extdns-, external-dns writes both:
|
||||||
|
// extdns-<fqdn> and extdns-<rrtype-lc>-<fqdn>
|
||||||
|
"extdns-" + base + ".",
|
||||||
|
"extdns-" + strings.ToLower(rrType) + "-" + base + ".",
|
||||||
|
}
|
||||||
|
for _, name := range candidates {
|
||||||
|
vals, err := getTXTValues(ctx, c, zoneID, name)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, raw := range vals {
|
||||||
|
v := strings.TrimSpace(raw)
|
||||||
|
// strip surrounding quotes if present
|
||||||
|
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||||
|
if unq, err := strconv.Unquote(v); err == nil {
|
||||||
|
v = unq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meta := parseExternalDNSMeta(v)
|
||||||
|
if meta == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if meta["heritage"] == "external-dns" &&
|
||||||
|
meta["external-dns/owner"] != "" &&
|
||||||
|
meta["external-dns/owner"] != externalDNSPoisonOwner {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExternalDNSMeta parses the comma-separated external-dns TXT format into a small map.
|
||||||
|
func parseExternalDNSMeta(v string) map[string]string {
|
||||||
|
parts := strings.Split(v, ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
meta := make(map[string]string, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kv := strings.SplitN(p, "=", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta[kv[0]] = kv[1]
|
||||||
|
}
|
||||||
|
if len(meta) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// build poison TXT records so external-dns thinks some *other* owner manages this
|
||||||
|
func buildExternalDNSPoisonTXTChanges(fqdn, rrType string) []r53types.Change {
|
||||||
|
base := strings.TrimSuffix(fqdn, ".")
|
||||||
|
names := []string{
|
||||||
|
"extdns-" + base + ".",
|
||||||
|
"extdns-" + strings.ToLower(rrType) + "-" + base + ".",
|
||||||
|
}
|
||||||
|
val := strconv.Quote(externalDNSPoisonValue)
|
||||||
|
changes := make([]r53types.Change, 0, len(names))
|
||||||
|
for _, n := range names {
|
||||||
|
changes = append(changes, r53types.Change{
|
||||||
|
Action: r53types.ChangeActionUpsert,
|
||||||
|
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||||
|
Name: aws.String(n),
|
||||||
|
Type: r53types.RRTypeTxt,
|
||||||
|
TTL: aws.Int64(300),
|
||||||
|
ResourceRecords: []r53types.ResourceRecord{
|
||||||
|
{Value: aws.String(val)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
/************* misc utils *************/
|
||||||
|
|
||||||
|
func truncateErr(s string) string {
|
||||||
|
const max = 2000
|
||||||
|
if len(s) > max {
|
||||||
|
return s[:max]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict unmarshal that treats "null" -> zero value correctly.
|
||||||
|
func jsonUnmarshalStrict(b []byte, dst any) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return errors.New("empty json")
|
||||||
|
}
|
||||||
|
return json.Unmarshal(b, dst)
|
||||||
|
}
|
||||||
@@ -371,6 +371,21 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
||||||
|
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||||
|
secure = strings.EqualFold(xf, "https")
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "ag_jwt",
|
||||||
|
Value: "Bearer " + access,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Secure: secure,
|
||||||
|
MaxAge: int((time.Hour * 8).Seconds()),
|
||||||
|
})
|
||||||
|
|
||||||
utils.WriteJSON(w, 200, dto.TokenPair{
|
utils.WriteJSON(w, 200, dto.TokenPair{
|
||||||
AccessToken: access,
|
AccessToken: access,
|
||||||
RefreshToken: newPair.Plain,
|
RefreshToken: newPair.Plain,
|
||||||
|
|||||||
802
internal/handlers/dns.go
Normal file
802
internal/handlers/dns.go
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------- Helpers ----------
|
||||||
|
|
||||||
|
func normLowerNoDot(s string) string {
|
||||||
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
|
return strings.TrimSuffix(s, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fqdn(domain string, rel string) string {
|
||||||
|
d := normLowerNoDot(domain)
|
||||||
|
r := normLowerNoDot(rel)
|
||||||
|
if r == "" || r == "@" {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return r + "." + d
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalJSONAny(v any) ([]byte, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var anyv any
|
||||||
|
if err := json.Unmarshal(b, &anyv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return marshalSortedDNS(anyv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalSortedDNS(v any) ([]byte, error) {
|
||||||
|
switch vv := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
keys := make([]string, 0, len(vv))
|
||||||
|
for k := range vv {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sortStrings(keys)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteByte('{')
|
||||||
|
for i, k := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
kb, _ := json.Marshal(k)
|
||||||
|
buf.Write(kb)
|
||||||
|
buf.WriteByte(':')
|
||||||
|
b, err := marshalSortedDNS(vv[k])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
case []any:
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, e := range vv {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
b, err := marshalSortedDNS(e)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
default:
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortStrings(a []string) {
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
for j := i + 1; j < len(a); j++ {
|
||||||
|
if a[j] < a[i] {
|
||||||
|
a[i], a[j] = a[j], a[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256HexBytes(b []byte) string {
|
||||||
|
sum := sha256.Sum256(b)
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fingerprint (provider-agnostic) */
|
||||||
|
type desiredRecord struct {
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
FQDN string `json:"fqdn"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TTL *int `json:"ttl,omitempty"`
|
||||||
|
Values []string `json:"values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeFingerprint(zoneID, fqdn, typ string, ttl *int, values datatypes.JSON) (string, error) {
|
||||||
|
var vals []string
|
||||||
|
if len(values) > 0 && string(values) != "null" {
|
||||||
|
if err := json.Unmarshal(values, &vals); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
sortStrings(vals)
|
||||||
|
}
|
||||||
|
payload := &desiredRecord{
|
||||||
|
ZoneID: zoneID, FQDN: fqdn, Type: strings.ToUpper(typ), TTL: ttl, Values: vals,
|
||||||
|
}
|
||||||
|
can, err := canonicalJSONAny(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return sha256HexBytes(can), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustSameOrgDomainWithCredential(db *gorm.DB, orgID uuid.UUID, credID uuid.UUID) error {
|
||||||
|
var cred models.Credential
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", credID, orgID).First(&cred).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fmt.Errorf("credential not found or belongs to different org")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cred.Provider != "aws" || cred.ScopeKind != "service" {
|
||||||
|
return fmt.Errorf("credential must be AWS Route 53 service scoped")
|
||||||
|
}
|
||||||
|
var scope map[string]any
|
||||||
|
if err := json.Unmarshal(cred.Scope, &scope); err != nil {
|
||||||
|
return fmt.Errorf("credential scope invalid json: %w", err)
|
||||||
|
}
|
||||||
|
if strings.ToLower(fmt.Sprint(scope["service"])) != "route53" {
|
||||||
|
return fmt.Errorf("credential scope.service must be route53")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Domain Handlers ----------
|
||||||
|
|
||||||
|
// ListDomains godoc
|
||||||
|
//
|
||||||
|
// @ID ListDomains
|
||||||
|
// @Summary List domains (org scoped)
|
||||||
|
// @Description Returns domains for X-Org-ID. Filters: `domain_name`, `status`, `q` (contains).
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param domain_name query string false "Exact domain name (lowercase, no trailing dot)"
|
||||||
|
// @Param status query string false "pending|provisioning|ready|failed"
|
||||||
|
// @Param q query string false "Domain contains (case-insensitive)"
|
||||||
|
// @Success 200 {array} dto.DomainResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /dns/domains [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListDomains(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := db.Model(&models.Domain{}).Where("organization_id = ?", orgID)
|
||||||
|
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("domain_name"))); v != "" {
|
||||||
|
q = q.Where("LOWER(domain_name) = ?", v)
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("status"))); v != "" {
|
||||||
|
q = q.Where("status = ?", v)
|
||||||
|
}
|
||||||
|
if needle := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("q"))); needle != "" {
|
||||||
|
q = q.Where("LOWER(domain_name) LIKE ?", "%"+needle+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.Domain
|
||||||
|
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]dto.DomainResponse, 0, len(rows))
|
||||||
|
for i := range rows {
|
||||||
|
out = append(out, domainOut(&rows[i]))
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomain godoc
|
||||||
|
//
|
||||||
|
// @ID GetDomain
|
||||||
|
// @Summary Get a domain (org scoped)
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Domain ID (UUID)"
|
||||||
|
// @Success 200 {object} dto.DomainResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /dns/domains/{id} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func GetDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var row models.Domain
|
||||||
|
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, domainOut(&row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDomain godoc
|
||||||
|
//
|
||||||
|
// @ID CreateDomain
|
||||||
|
// @Summary Create a domain (org scoped)
|
||||||
|
// @Description Creates a domain bound to a Route 53 scoped credential. Archer will backfill ZoneID if omitted.
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param body body dto.CreateDomainRequest true "Domain payload"
|
||||||
|
// @Success 201 {object} dto.DomainResponse
|
||||||
|
// @Failure 400 {string} string "validation error"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /dns/domains [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func CreateDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var in dto.CreateDomainRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dto.DNSValidate(in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
credID, _ := uuid.Parse(in.CredentialID)
|
||||||
|
if err := mustSameOrgDomainWithCredential(db, orgID, credID); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_credential", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
row := &models.Domain{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
DomainName: normLowerNoDot(in.DomainName),
|
||||||
|
ZoneID: strings.TrimSpace(in.ZoneID),
|
||||||
|
Status: "pending",
|
||||||
|
LastError: "",
|
||||||
|
CredentialID: credID,
|
||||||
|
}
|
||||||
|
if err := db.Create(row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, domainOut(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDomain godoc
|
||||||
|
//
|
||||||
|
// @ID UpdateDomain
|
||||||
|
// @Summary Update a domain (org scoped)
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Domain ID (UUID)"
|
||||||
|
// @Param body body dto.UpdateDomainRequest true "Fields to update"
|
||||||
|
// @Success 200 {object} dto.DomainResponse
|
||||||
|
// @Failure 400 {string} string "validation error"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /dns/domains/{id} [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var row models.Domain
|
||||||
|
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var in dto.UpdateDomainRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dto.DNSValidate(in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if in.DomainName != nil {
|
||||||
|
row.DomainName = normLowerNoDot(*in.DomainName)
|
||||||
|
}
|
||||||
|
if in.CredentialID != nil {
|
||||||
|
credID, _ := uuid.Parse(*in.CredentialID)
|
||||||
|
if err := mustSameOrgDomainWithCredential(db, orgID, credID); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_credential", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.CredentialID = credID
|
||||||
|
row.Status = "pending"
|
||||||
|
row.LastError = ""
|
||||||
|
}
|
||||||
|
if in.ZoneID != nil {
|
||||||
|
row.ZoneID = strings.TrimSpace(*in.ZoneID)
|
||||||
|
}
|
||||||
|
if in.Status != nil {
|
||||||
|
row.Status = *in.Status
|
||||||
|
if row.Status == "pending" {
|
||||||
|
row.LastError = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Save(&row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, domainOut(&row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDomain godoc
|
||||||
|
//
|
||||||
|
// @ID DeleteDomain
|
||||||
|
// @Summary Delete a domain
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Domain ID (UUID)"
|
||||||
|
// @Success 204
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /dns/domains/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DeleteDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := db.Where("organization_id = ? AND id = ?", orgID, id).Delete(&models.Domain{})
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Record Set Handlers ----------
|
||||||
|
|
||||||
|
// ListRecordSets godoc
|
||||||
|
//
|
||||||
|
// @ID ListRecordSets
|
||||||
|
// @Summary List record sets for a domain
|
||||||
|
// @Description Filters: `name`, `type`, `status`.
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param domain_id path string true "Domain ID (UUID)"
|
||||||
|
// @Param name query string false "Exact relative name or FQDN (server normalizes)"
|
||||||
|
// @Param type query string false "RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)"
|
||||||
|
// @Param status query string false "pending|provisioning|ready|failed"
|
||||||
|
// @Success 200 {array} dto.RecordSetResponse
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "domain not found"
|
||||||
|
// @Router /dns/domains/{domain_id}/records [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListRecordSets(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
did, err := uuid.Parse(chi.URLParam(r, "domain_id"))
|
||||||
|
if err != nil {
|
||||||
|
log.Info().Msg(err.Error())
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid domain UUID:")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var domain models.Domain
|
||||||
|
if err := db.Where("organization_id = ? AND id = ?", orgID, did).First(&domain).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := db.Model(&models.RecordSet{}).Where("domain_id = ?", did)
|
||||||
|
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("name"))); v != "" {
|
||||||
|
dn := strings.ToLower(domain.DomainName)
|
||||||
|
rel := v
|
||||||
|
// normalize apex or FQDN into relative
|
||||||
|
if v == dn || v == dn+"." {
|
||||||
|
rel = ""
|
||||||
|
} else {
|
||||||
|
rel = strings.TrimSuffix(v, "."+dn)
|
||||||
|
rel = normLowerNoDot(rel)
|
||||||
|
}
|
||||||
|
q = q.Where("LOWER(name) = ?", rel)
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(strings.ToUpper(r.URL.Query().Get("type"))); v != "" {
|
||||||
|
q = q.Where("type = ?", v)
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("status"))); v != "" {
|
||||||
|
q = q.Where("status = ?", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.RecordSet
|
||||||
|
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]dto.RecordSetResponse, 0, len(rows))
|
||||||
|
for i := range rows {
|
||||||
|
out = append(out, recordOut(&rows[i]))
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRecordSet godoc
|
||||||
|
//
|
||||||
|
// @ID CreateRecordSet
|
||||||
|
// @Summary Create a record set (pending; Archer will UPSERT to Route 53)
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param domain_id path string true "Domain ID (UUID)"
|
||||||
|
// @Param body body dto.CreateRecordSetRequest true "Record set payload"
|
||||||
|
// @Success 201 {object} dto.RecordSetResponse
|
||||||
|
// @Failure 400 {string} string "validation error"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "domain not found"
|
||||||
|
// @Router /dns/domains/{domain_id}/records [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func CreateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
did, err := uuid.Parse(chi.URLParam(r, "domain_id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid domain UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var domain models.Domain
|
||||||
|
if err := db.Where("organization_id = ? AND id = ?", orgID, did).First(&domain).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in dto.CreateRecordSetRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dto.DNSValidate(in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := strings.ToUpper(in.Type)
|
||||||
|
if t == "CNAME" && len(in.Values) != 1 {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "CNAME requires exactly one value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rel := normLowerNoDot(in.Name)
|
||||||
|
fq := fqdn(domain.DomainName, rel)
|
||||||
|
|
||||||
|
// Pre-flight: block duplicate tuple and protect from non-autoglue rows
|
||||||
|
var existing models.RecordSet
|
||||||
|
if err := db.Where("domain_id = ? AND LOWER(name) = ? AND type = ?",
|
||||||
|
domain.ID, strings.ToLower(rel), t).First(&existing).Error; err == nil {
|
||||||
|
if existing.Owner != "" && existing.Owner != "autoglue" {
|
||||||
|
utils.WriteError(w, http.StatusConflict, "ownership_conflict",
|
||||||
|
"record with the same (name,type) exists but is not owned by autoglue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusConflict, "already_exists",
|
||||||
|
"a record with the same (name,type) already exists; use PATCH to modify")
|
||||||
|
return
|
||||||
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesJSON, _ := json.Marshal(in.Values)
|
||||||
|
fp, err := computeFingerprint(domain.ZoneID, fq, t, in.TTL, datatypes.JSON(valuesJSON))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "fingerprint_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
row := &models.RecordSet{
|
||||||
|
DomainID: domain.ID,
|
||||||
|
Name: rel,
|
||||||
|
Type: t,
|
||||||
|
TTL: in.TTL,
|
||||||
|
Values: datatypes.JSON(valuesJSON),
|
||||||
|
Fingerprint: fp,
|
||||||
|
Status: "pending",
|
||||||
|
LastError: "",
|
||||||
|
Owner: "autoglue",
|
||||||
|
}
|
||||||
|
if err := db.Create(row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, recordOut(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRecordSet godoc
|
||||||
|
//
|
||||||
|
// @ID UpdateRecordSet
|
||||||
|
// @Summary Update a record set (flips to pending for reconciliation)
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Record Set ID (UUID)"
|
||||||
|
// @Param body body dto.UpdateRecordSetRequest true "Fields to update"
|
||||||
|
// @Success 200 {object} dto.RecordSetResponse
|
||||||
|
// @Failure 400 {string} string "validation error"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /dns/records/{id} [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.RecordSet
|
||||||
|
if err := db.
|
||||||
|
Joins("Domain").
|
||||||
|
Where(`record_sets.id = ? AND "Domain"."organization_id" = ?`, id, orgID).
|
||||||
|
First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "record set not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var domain models.Domain
|
||||||
|
if err := db.Where("id = ?", row.DomainID).First(&domain).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in dto.UpdateRecordSetRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dto.DNSValidate(in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if row.Owner != "" && row.Owner != "autoglue" {
|
||||||
|
utils.WriteError(w, http.StatusConflict, "ownership_conflict",
|
||||||
|
"record is not owned by autoglue; refuse to modify")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
if in.Name != nil {
|
||||||
|
row.Name = normLowerNoDot(*in.Name)
|
||||||
|
}
|
||||||
|
if in.Type != nil {
|
||||||
|
row.Type = strings.ToUpper(*in.Type)
|
||||||
|
}
|
||||||
|
if in.TTL != nil {
|
||||||
|
row.TTL = in.TTL
|
||||||
|
}
|
||||||
|
if in.Values != nil {
|
||||||
|
t := row.Type
|
||||||
|
if in.Type != nil {
|
||||||
|
t = strings.ToUpper(*in.Type)
|
||||||
|
}
|
||||||
|
if t == "CNAME" && len(*in.Values) != 1 {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "CNAME requires exactly one value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(*in.Values)
|
||||||
|
row.Values = datatypes.JSON(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Status != nil {
|
||||||
|
row.Status = *in.Status
|
||||||
|
} else {
|
||||||
|
row.Status = "pending"
|
||||||
|
row.LastError = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fq := fqdn(domain.DomainName, row.Name)
|
||||||
|
fp, err := computeFingerprint(domain.ZoneID, fq, row.Type, row.TTL, row.Values)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "fingerprint_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.Fingerprint = fp
|
||||||
|
|
||||||
|
if err := db.Save(&row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, recordOut(&row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRecordSet godoc
|
||||||
|
//
|
||||||
|
// @ID DeleteRecordSet
|
||||||
|
// @Summary Delete a record set (API removes row; worker can optionally handle external deletion policy)
|
||||||
|
// @Tags DNS
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Record Set ID (UUID)"
|
||||||
|
// @Success 204
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /dns/records/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DeleteRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sub := db.Model(&models.RecordSet{}).
|
||||||
|
Select("record_sets.id").
|
||||||
|
Joins("JOIN domains ON domains.id = record_sets.domain_id").
|
||||||
|
Where("record_sets.id = ? AND domains.organization_id = ?", id, orgID)
|
||||||
|
|
||||||
|
res := db.Where("id IN (?)", sub).Delete(&models.RecordSet{})
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "record set not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Out mappers ----------
|
||||||
|
|
||||||
|
func domainOut(m *models.Domain) dto.DomainResponse {
|
||||||
|
return dto.DomainResponse{
|
||||||
|
ID: m.ID.String(),
|
||||||
|
OrganizationID: m.OrganizationID.String(),
|
||||||
|
DomainName: m.DomainName,
|
||||||
|
ZoneID: m.ZoneID,
|
||||||
|
Status: m.Status,
|
||||||
|
LastError: m.LastError,
|
||||||
|
CredentialID: m.CredentialID.String(),
|
||||||
|
CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: m.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordOut(r *models.RecordSet) dto.RecordSetResponse {
|
||||||
|
vals := r.Values
|
||||||
|
if len(vals) == 0 {
|
||||||
|
vals = datatypes.JSON("[]")
|
||||||
|
}
|
||||||
|
return dto.RecordSetResponse{
|
||||||
|
ID: r.ID.String(),
|
||||||
|
DomainID: r.DomainID.String(),
|
||||||
|
Name: r.Name,
|
||||||
|
Type: r.Type,
|
||||||
|
TTL: r.TTL,
|
||||||
|
Values: []byte(vals),
|
||||||
|
Fingerprint: r.Fingerprint,
|
||||||
|
Status: r.Status,
|
||||||
|
LastError: r.LastError,
|
||||||
|
Owner: r.Owner,
|
||||||
|
CreatedAt: r.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: r.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
103
internal/handlers/dto/dns.go
Normal file
103
internal/handlers/dto/dns.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dnsValidate = validator.New()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_ = dnsValidate.RegisterValidation("fqdn", func(fl validator.FieldLevel) bool {
|
||||||
|
s := strings.TrimSpace(fl.Field().String())
|
||||||
|
if s == "" || len(s) > 253 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Minimal: lower-cased, no trailing dot in our API (normalize server-side)
|
||||||
|
// You can add stricter checks later.
|
||||||
|
return !strings.HasPrefix(s, ".") && !strings.Contains(s, "..")
|
||||||
|
})
|
||||||
|
_ = dnsValidate.RegisterValidation("rrtype", func(fl validator.FieldLevel) bool {
|
||||||
|
switch strings.ToUpper(fl.Field().String()) {
|
||||||
|
case "A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Domains ----
|
||||||
|
|
||||||
|
type CreateDomainRequest struct {
|
||||||
|
DomainName string `json:"domain_name" validate:"required,fqdn"`
|
||||||
|
CredentialID string `json:"credential_id" validate:"required,uuid4"`
|
||||||
|
ZoneID string `json:"zone_id,omitempty" validate:"omitempty,max=128"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDomainRequest struct {
|
||||||
|
CredentialID *string `json:"credential_id,omitempty" validate:"omitempty,uuid4"`
|
||||||
|
ZoneID *string `json:"zone_id,omitempty" validate:"omitempty,max=128"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending provisioning ready failed"`
|
||||||
|
DomainName *string `json:"domain_name,omitempty" validate:"omitempty,fqdn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
OrganizationID string `json:"organization_id"`
|
||||||
|
DomainName string `json:"domain_name"`
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LastError string `json:"last_error"`
|
||||||
|
CredentialID string `json:"credential_id"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Record Sets ----
|
||||||
|
|
||||||
|
type AliasTarget struct {
|
||||||
|
HostedZoneID string `json:"hosted_zone_id" validate:"required"`
|
||||||
|
DNSName string `json:"dns_name" validate:"required"`
|
||||||
|
EvaluateTargetHealth bool `json:"evaluate_target_health"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRecordSetRequest struct {
|
||||||
|
// Name relative to domain ("endpoint") OR FQDN ("endpoint.example.com").
|
||||||
|
// Server normalizes to relative.
|
||||||
|
Name string `json:"name" validate:"required,max=253"`
|
||||||
|
Type string `json:"type" validate:"required,rrtype"`
|
||||||
|
TTL *int `json:"ttl,omitempty" validate:"omitempty,gte=1,lte=86400"`
|
||||||
|
Values []string `json:"values" validate:"omitempty,dive,min=1,max=1024"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRecordSetRequest struct {
|
||||||
|
// Any change flips status back to pending (worker will UPSERT)
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,max=253"`
|
||||||
|
Type *string `json:"type,omitempty" validate:"omitempty,rrtype"`
|
||||||
|
TTL *int `json:"ttl,omitempty" validate:"omitempty,gte=1,lte=86400"`
|
||||||
|
Values *[]string `json:"values,omitempty" validate:"omitempty,dive,min=1,max=1024"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending provisioning ready failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordSetResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DomainID string `json:"domain_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TTL *int `json:"ttl,omitempty"`
|
||||||
|
Values json.RawMessage `json:"values" swaggertype:"object"` // []string JSON
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LastError string `json:"last_error"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSValidate Quick helper to validate DTOs in handlers
|
||||||
|
func DNSValidate(i any) error {
|
||||||
|
return dnsValidate.Struct(i)
|
||||||
|
}
|
||||||
@@ -370,6 +370,63 @@ func DeleteServer(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetServerHostKey godoc
|
||||||
|
//
|
||||||
|
// @ID ResetServerHostKey
|
||||||
|
// @Summary Reset SSH host key (org scoped)
|
||||||
|
// @Description Clears the stored SSH host key for this server. The next SSH connection will re-learn the host key (trust-on-first-use).
|
||||||
|
// @Tags Servers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Server ID (UUID)"
|
||||||
|
// @Success 200 {object} dto.ServerResponse
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "reset failed"
|
||||||
|
// @Router /servers/{id}/reset-hostkey [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ResetServerHostKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "id_invalid", "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var server models.Server
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&server).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear stored host key so next SSH handshake will TOFU and persist a new one.
|
||||||
|
server.SSHHostKey = ""
|
||||||
|
server.SSHHostKeyAlgo = ""
|
||||||
|
|
||||||
|
if err := db.Save(&server).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to reset host key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
func validStatus(status string) bool {
|
func validStatus(status string) bool {
|
||||||
|
|||||||
18
internal/models/backup.go
Normal file
18
internal/models/backup.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backup struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_credential,priority:1"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
Enabled bool `gorm:"not null;default:false" json:"enabled"`
|
||||||
|
CredentialID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:uniq_org_credential,priority:2" json:"credential_id"`
|
||||||
|
Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||||
|
}
|
||||||
@@ -4,18 +4,38 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/datatypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Domain struct {
|
type Domain struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_credentials_org_provider" json:"organization_id"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_domain,priority:1"`
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
ClusterID *uuid.UUID `gorm:"type:uuid" json:"cluster_id,omitempty"`
|
DomainName string `gorm:"type:varchar(253);not null;uniqueIndex:uniq_org_domain,priority:2"`
|
||||||
Cluster *Cluster `gorm:"foreignKey:ClusterID" json:"cluster,omitempty"`
|
ZoneID string `gorm:"type:varchar(128);not null;default:''"` // backfilled for R53 (e.g. "/hostedzone/Z123...")
|
||||||
DomainName string `gorm:"not null;index" json:"domain_name,omitempty"`
|
Status string `gorm:"type:varchar(20);not null;default:'pending'"` // pending, provisioning, ready, failed
|
||||||
DomainID string
|
LastError string `gorm:"type:text;not null;default:''"`
|
||||||
CredentialID uuid.UUID `gorm:"type:uuid;not null" json:"credential_id"`
|
CredentialID uuid.UUID `gorm:"type:uuid;not null" json:"credential_id"`
|
||||||
Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"`
|
Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordSet struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||||
|
DomainID uuid.UUID `gorm:"type:uuid;not null;index"`
|
||||||
|
Domain Domain `gorm:"foreignKey:DomainID;constraint:OnDelete:CASCADE"`
|
||||||
|
Name string `gorm:"type:varchar(253);not null"` // e.g. "endpoint" (relative to DomainName)
|
||||||
|
Type string `gorm:"type:varchar(10);not null;index"` // A, AAAA, CNAME, TXT, MX, SRV, NS, CAA...
|
||||||
|
TTL *int `gorm:""` // nil for alias targets (Route 53 ignores TTL for alias)
|
||||||
|
Values datatypes.JSON `gorm:"type:jsonb;not null;default:'[]'"`
|
||||||
|
Fingerprint string `gorm:"type:char(64);not null;index"` // sha256 of canonical(name,type,ttl,values|alias)
|
||||||
|
Status string `gorm:"type:varchar(20);not null;default:'pending'"`
|
||||||
|
Owner string `gorm:"type:varchar(16);not null;default:'unknown'"` // 'autoglue' | 'external' | 'unknown'
|
||||||
|
LastError string `gorm:"type:text;not null;default:''"`
|
||||||
|
_ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:1"` // tag holder
|
||||||
|
_ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:2"`
|
||||||
|
_ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:3"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type Server struct {
|
|||||||
Role string `gorm:"not null" json:"role" enums:"master,worker,bastion"` // e.g., "master", "worker", "bastion"
|
Role string `gorm:"not null" json:"role" enums:"master,worker,bastion"` // e.g., "master", "worker", "bastion"
|
||||||
Status string `gorm:"default:'pending'" json:"status" enums:"pending, provisioning, ready, failed"` // pending, provisioning, ready, failed
|
Status string `gorm:"default:'pending'" json:"status" enums:"pending, provisioning, ready, failed"` // pending, provisioning, ready, failed
|
||||||
NodePools []NodePool `gorm:"many2many:node_servers;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
NodePools []NodePool `gorm:"many2many:node_servers;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||||
|
SSHHostKey string `gorm:"column:ssh_host_key"`
|
||||||
|
SSHHostKeyAlgo string `gorm:"column:ssh_host_key_algo"`
|
||||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"embed"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed pgwebbin/*
|
|
||||||
var pgwebFS embed.FS
|
|
||||||
|
|
||||||
type pgwebAsset struct {
|
|
||||||
Path string
|
|
||||||
SHA256 string
|
|
||||||
}
|
|
||||||
|
|
||||||
var pgwebIndex = map[string]pgwebAsset{
|
|
||||||
"linux/amd64": {Path: "pgwebbin/pgweb-linux-amd64", SHA256: ""},
|
|
||||||
"linux/arm64": {Path: "pgwebbin/pgweb-linux-arm64", SHA256: ""},
|
|
||||||
"darwin/amd64": {Path: "pgwebbin/pgweb-darwin-amd64", SHA256: ""},
|
|
||||||
"darwin/arm64": {Path: "pgwebbin/pgweb-darwin-arm64", SHA256: ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtractPgweb() (string, error) {
|
|
||||||
key := runtime.GOOS + "/" + runtime.GOARCH
|
|
||||||
as, ok := pgwebIndex[key]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("pgweb not embedded for %s", key)
|
|
||||||
}
|
|
||||||
f, err := pgwebFS.Open(as.Path)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("embedded pgweb missing: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
tmpDir, err := os.MkdirTemp("", "pgweb-*")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := "pgweb"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
filename += ".exe"
|
|
||||||
}
|
|
||||||
outPath := filepath.Join(tmpDir, filename)
|
|
||||||
|
|
||||||
out, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o700)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
h := sha256.New()
|
|
||||||
if _, err = io.Copy(io.MultiWriter(out, h), f); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if as.SHA256 != "" {
|
|
||||||
got := hex.EncodeToString(h.Sum(nil))
|
|
||||||
if got != as.SHA256 {
|
|
||||||
return "", fmt.Errorf("pgweb checksum mismatch: got=%s want=%s", got, as.SHA256)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure it’s executable on Unix; Windows ignores this.
|
|
||||||
_ = os.Chmod(outPath, 0o700)
|
|
||||||
return outPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CleanupPgweb(pgwebPath string) error {
|
|
||||||
if pgwebPath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dir := filepath.Dir(pgwebPath)
|
|
||||||
if dir == "" || dir == "/" || dir == "." {
|
|
||||||
return errors.New("refusing to remove suspicious directory")
|
|
||||||
}
|
|
||||||
return os.RemoveAll(dir)
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Pgweb struct {
|
|
||||||
cmd *exec.Cmd
|
|
||||||
host string
|
|
||||||
port string
|
|
||||||
bin string
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartPgweb(dbURL, host, port string, readonly bool, user, pass string) (*Pgweb, error) {
|
|
||||||
// pick random port if 0/empty
|
|
||||||
if port == "" || port == "0" {
|
|
||||||
l, err := net.Listen("tcp", net.JoinHostPort(host, "0"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer l.Close()
|
|
||||||
_, p, _ := net.SplitHostPort(l.Addr().String())
|
|
||||||
port = p
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"--url", dbURL,
|
|
||||||
"--bind", host,
|
|
||||||
"--listen", port,
|
|
||||||
"--prefix", "db-studio",
|
|
||||||
"--skip-open",
|
|
||||||
}
|
|
||||||
if readonly {
|
|
||||||
args = append(args, "--readonly")
|
|
||||||
}
|
|
||||||
if user != "" && pass != "" {
|
|
||||||
args = append(args, "--auth-user", user, "--auth-pass", pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
pgwebBinary, err := ExtractPgweb()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("pgweb extract: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(pgwebBinary, args...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for port to be ready
|
|
||||||
deadline := time.Now().Add(4 * time.Second)
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
c, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 200*time.Millisecond)
|
|
||||||
if err == nil {
|
|
||||||
_ = c.Close()
|
|
||||||
return &Pgweb{cmd: cmd, host: host, port: port}, nil
|
|
||||||
}
|
|
||||||
time.Sleep(120 * time.Millisecond)
|
|
||||||
}
|
|
||||||
// still return object so caller can Stop()
|
|
||||||
//return &Pgweb{cmd: cmd, host: host, port: port, bin: pgwebBinary}, nil
|
|
||||||
return nil, fmt.Errorf("pgweb did not become ready on %s:%s", host, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pgweb) Proxy() http.HandlerFunc {
|
|
||||||
target, _ := url.Parse("http://" + net.JoinHostPort(p.host, p.port))
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
||||||
proxy.FlushInterval = 100 * time.Millisecond
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r.Host = target.Host
|
|
||||||
// Let pgweb handle its paths; we mount it at a prefix.
|
|
||||||
proxy.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pgweb) Stop(ctx context.Context) error {
|
|
||||||
if p == nil || p.cmd == nil || p.cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_ = p.cmd.Process.Kill()
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() { _, _ = p.cmd.Process.Wait(); close(done) }()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
if p.bin != "" {
|
|
||||||
_ = CleanupPgweb(p.bin)
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pgweb) Port() string {
|
|
||||||
return p.port
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -27,22 +27,22 @@ func main() {
|
|||||||
{
|
{
|
||||||
Name: "pgweb-linux-amd64",
|
Name: "pgweb-linux-amd64",
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_amd64.zip", version),
|
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_amd64.zip", version),
|
||||||
SHA256: "",
|
SHA256: "3d6c2063e1040b8a625eb7c43c9b84f8ed12cfc9a798eacbce85179963ee2554",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "pgweb-linux-arm64",
|
Name: "pgweb-linux-arm64",
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_arm64.zip", version),
|
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_arm64.zip", version),
|
||||||
SHA256: "",
|
SHA256: "079c698a323ed6431ce7e6343ee5847c7da62afbf45dfb2e78f8289d7b381783",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "pgweb-darwin-amd64",
|
Name: "pgweb-darwin-amd64",
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_amd64.zip", version),
|
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_amd64.zip", version),
|
||||||
SHA256: "",
|
SHA256: "c0a098e2eb9cf9f7c20161a2947522eb67eacbf2b6c3389c2f8e8c5ed7238957",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "pgweb-darwin-arm64",
|
Name: "pgweb-darwin-arm64",
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_arm64.zip", version),
|
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_arm64.zip", version),
|
||||||
SHA256: "",
|
SHA256: "c8f5fca847f461ba22a619e2d96cb1656cefdffd8f2aef2340e14fc5b518d3a2",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,67 +105,33 @@ func fileSHA256(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func unzipSingle(zipPath, outPath string) error {
|
func unzipSingle(zipPath, outPath string) error {
|
||||||
// minimal unzip: because pgweb zip has only one binary
|
zr, err := zip.OpenReader(zipPath)
|
||||||
r, err := os.Open(zipPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
// use archive/zip
|
|
||||||
stat, err := os.Stat(zipPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return unzipFile(zipPath, outPath, stat.Size())
|
|
||||||
}
|
|
||||||
|
|
||||||
func unzipFile(zipFile, outFile string, _ int64) error {
|
|
||||||
r, err := os.Open(zipFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
fi, _ := r.Stat()
|
|
||||||
|
|
||||||
// rely on standard zip reader
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpZip := filepath.Join(os.TempDir(), fi.Name())
|
|
||||||
if err := os.WriteFile(tmpZip, data, 0o644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpZip)
|
|
||||||
|
|
||||||
zr, err := os.Open(tmpZip)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer zr.Close()
|
defer zr.Close()
|
||||||
// extract using standard lib
|
|
||||||
zr2, err := zip.OpenReader(tmpZip)
|
if len(zr.File) == 0 {
|
||||||
|
return fmt.Errorf("zip file %s is empty", zipPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := zr.File[0]
|
||||||
|
|
||||||
|
rc, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer zr2.Close()
|
defer rc.Close()
|
||||||
for _, f := range zr2.File {
|
|
||||||
rc, err := f.Open()
|
out, err := os.Create(outPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
out, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, rc); err != nil {
|
|
||||||
out.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
out.Close()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, rc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ProtectedRoute } from "@/components/protected-route.tsx"
|
|||||||
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
||||||
import { Login } from "@/pages/auth/login.tsx"
|
import { Login } from "@/pages/auth/login.tsx"
|
||||||
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
||||||
|
import { DnsPage } from "@/pages/dns/dns-page.tsx"
|
||||||
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
||||||
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
||||||
import { MePage } from "@/pages/me/me-page.tsx"
|
import { MePage } from "@/pages/me/me-page.tsx"
|
||||||
@@ -35,6 +36,7 @@ export default function App() {
|
|||||||
<Route path="/annotations" element={<AnnotationPage />} />
|
<Route path="/annotations" element={<AnnotationPage />} />
|
||||||
<Route path="/node-pools" element={<NodePoolsPage />} />
|
<Route path="/node-pools" element={<NodePoolsPage />} />
|
||||||
<Route path="/credentials" element={<CredentialPage />} />
|
<Route path="/credentials" element={<CredentialPage />} />
|
||||||
|
<Route path="/dns" element={<DnsPage />} />
|
||||||
|
|
||||||
<Route path="/admin/jobs" element={<JobsPage />} />
|
<Route path="/admin/jobs" element={<JobsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
49
ui/src/api/dns.ts
Normal file
49
ui/src/api/dns.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { withRefresh } from "@/api/with-refresh.ts"
|
||||||
|
import type {
|
||||||
|
DtoCreateDomainRequest,
|
||||||
|
DtoCreateRecordSetRequest,
|
||||||
|
DtoUpdateDomainRequest,
|
||||||
|
DtoUpdateRecordSetRequest,
|
||||||
|
} from "@/sdk"
|
||||||
|
import { makeDnsApi } from "@/sdkClient.ts"
|
||||||
|
|
||||||
|
const dns = makeDnsApi()
|
||||||
|
|
||||||
|
export const dnsApi = {
|
||||||
|
listDomains: () =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.listDomains()
|
||||||
|
}),
|
||||||
|
getDomain: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.getDomain({ id })
|
||||||
|
}),
|
||||||
|
createDomain: async (body: DtoCreateDomainRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.createDomain({ body })
|
||||||
|
}),
|
||||||
|
updateDomain: async (id: string, body: DtoUpdateDomainRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.updateDomain({ id, body })
|
||||||
|
}),
|
||||||
|
deleteDomain: async (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.deleteDomain({ id })
|
||||||
|
}),
|
||||||
|
listRecordSetsByDomain: async (domainId: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.listRecordSets({ domainId })
|
||||||
|
}),
|
||||||
|
createRecordSetsByDomain: async (domainId: string, body: DtoCreateRecordSetRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.createRecordSet({ domainId, body })
|
||||||
|
}),
|
||||||
|
updateRecordSetsByDomain: async (id: string, body: DtoUpdateRecordSetRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.updateRecordSet({ id, body })
|
||||||
|
}),
|
||||||
|
deleteRecordSetsByDomain: async (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await dns.deleteRecordSet({ id })
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { AiOutlineCluster } from "react-icons/ai"
|
import { AiOutlineCluster } from "react-icons/ai"
|
||||||
import { GrUserWorker } from "react-icons/gr"
|
import { GrUserWorker } from "react-icons/gr"
|
||||||
|
import { MdOutlineDns } from "react-icons/md"
|
||||||
|
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
to: string
|
to: string
|
||||||
@@ -23,6 +24,7 @@ export type NavItem = {
|
|||||||
|
|
||||||
export const mainNav: NavItem[] = [
|
export const mainNav: NavItem[] = [
|
||||||
{ to: "/clusters", label: "Clusters", icon: AiOutlineCluster },
|
{ to: "/clusters", label: "Clusters", icon: AiOutlineCluster },
|
||||||
|
{ to: "/dns", label: "DNS", icon: MdOutlineDns },
|
||||||
{ to: "/node-pools", label: "Node Pools", icon: BoxesIcon },
|
{ to: "/node-pools", label: "Node Pools", icon: BoxesIcon },
|
||||||
{ to: "/annotations", label: "Annotations", icon: ComponentIcon },
|
{ to: "/annotations", label: "Annotations", icon: ComponentIcon },
|
||||||
{ to: "/labels", label: "Labels", icon: TagsIcon },
|
{ to: "/labels", label: "Labels", icon: TagsIcon },
|
||||||
|
|||||||
@@ -2,7 +2,16 @@ import { useMemo, useState } from "react"
|
|||||||
import { credentialsApi } from "@/api/credentials"
|
import { credentialsApi } from "@/api/credentials"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { AlertTriangle, Eye, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2, } from "lucide-react"
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
Eye,
|
||||||
|
Loader2,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react"
|
||||||
import { Controller, useForm } from "react-hook-form"
|
import { Controller, useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -20,16 +29,36 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
|
|||||||
1040
ui/src/pages/dns/dns-page.tsx
Normal file
1040
ui/src/pages/dns/dns-page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
|
import { credentialsApi } from "@/api/credentials.ts"
|
||||||
import { withRefresh } from "@/api/with-refresh.ts"
|
import { withRefresh } from "@/api/with-refresh.ts"
|
||||||
import { orgStore } from "@/auth/org.ts"
|
import { orgStore } from "@/auth/org.ts"
|
||||||
|
import type { DtoCredentialOut } from "@/sdk"
|
||||||
import { makeOrgsApi } from "@/sdkClient.ts"
|
import { makeOrgsApi } from "@/sdkClient.ts"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
@@ -20,6 +22,20 @@ import {
|
|||||||
} from "@/components/ui/form.tsx"
|
} from "@/components/ui/form.tsx"
|
||||||
import { Input } from "@/components/ui/input.tsx"
|
import { Input } from "@/components/ui/input.tsx"
|
||||||
|
|
||||||
|
const isS3 = (c: DtoCredentialOut) =>
|
||||||
|
c.provider === "aws" &&
|
||||||
|
c.scope_kind === "service" &&
|
||||||
|
// scope may be JSON; allow both object and stringified JSON
|
||||||
|
(() => {
|
||||||
|
const s = (c as any).scope
|
||||||
|
try {
|
||||||
|
const obj = typeof s === "string" ? JSON.parse(s) : s || {}
|
||||||
|
return obj?.service === "s3"
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().min(1, "Required"),
|
name: z.string().min(1, "Required"),
|
||||||
domain: z.string().optional(),
|
domain: z.string().optional(),
|
||||||
@@ -38,6 +54,13 @@ export const OrgSettings = () => {
|
|||||||
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
|
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const credentialQ = useQuery({
|
||||||
|
queryKey: ["credentials", "s3"],
|
||||||
|
queryFn: () => credentialsApi.listCredentials(), // client-side filter
|
||||||
|
})
|
||||||
|
|
||||||
|
const s3Credentials = useMemo(() => (credentialQ.data ?? []).filter(isS3), [credentialQ.data])
|
||||||
|
|
||||||
const form = useForm<Values>({
|
const form = useForm<Values>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
AuthApi,
|
AuthApi,
|
||||||
Configuration,
|
Configuration,
|
||||||
CredentialsApi,
|
CredentialsApi,
|
||||||
|
DNSApi,
|
||||||
LabelsApi,
|
LabelsApi,
|
||||||
MeApi,
|
MeApi,
|
||||||
MeAPIKeysApi,
|
MeAPIKeysApi,
|
||||||
@@ -118,3 +119,7 @@ export function makeMetaApi() {
|
|||||||
export function makeCredentialsApi() {
|
export function makeCredentialsApi() {
|
||||||
return makeApiClient(CredentialsApi)
|
return makeApiClient(CredentialsApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeDnsApi() {
|
||||||
|
return makeApiClient(DNSApi)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user