mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 12:50:05 +01:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
165d2a2af1 | ||
|
|
fc1c83ba18 | ||
|
|
2be0eb8180 | ||
|
|
81043419e1 | ||
|
|
f2ff08993a | ||
|
|
fabf456786 | ||
|
|
23c60d4ce8 | ||
|
|
c9bc09ae92 | ||
|
|
6ec8305962 | ||
|
|
8d34198477 | ||
|
|
f79224b831 | ||
|
|
cd8e3f2d86 | ||
|
|
ad0ec48027 | ||
|
|
7a3e56b3ff | ||
|
|
015044abb1 | ||
|
|
afd8c8ceb2 | ||
|
|
b358911b1b | ||
|
|
ad8141a497 | ||
|
|
9485b2ae4f | ||
|
|
cc8e8b38c7 | ||
|
|
586e51b8cc | ||
|
|
ea4c625269 | ||
|
|
b4c108a5be | ||
|
|
3a1ce33bca | ||
|
|
dbb7ec398e | ||
|
|
82847e5027 | ||
|
|
4314599427 | ||
|
|
9108ee8f8f | ||
|
|
1feb3e29e1 | ||
|
|
0e9ce98624 |
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: 'v2.2.4'
|
||||
|
||||
|
||||
3
Makefile
3
Makefile
@@ -310,6 +310,9 @@ doctor: ## Print environment diagnostics (shell, versions, generator availabilit
|
||||
$(OGC_BIN) version || true; \
|
||||
}
|
||||
|
||||
fetch-pgweb: ## Fetch PGWeb Binaries for embedding
|
||||
go run ./tools/pgweb_fetch.go
|
||||
|
||||
help: ## Show this help
|
||||
@grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
51
cmd/db.go
Normal file
51
cmd/db.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dbCmd = &cobra.Command{
|
||||
Use: "db",
|
||||
Short: "Database utilities",
|
||||
}
|
||||
|
||||
var dbPsqlCmd = &cobra.Command{
|
||||
Use: "psql",
|
||||
Short: "Open a psql session to the app database",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.DbURL == "" {
|
||||
return errors.New("database.url is empty")
|
||||
}
|
||||
psql := "psql"
|
||||
if runtime.GOOS == "windows" {
|
||||
psql = "psql.exe"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 72*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
psqlCmd := exec.CommandContext(ctx, psql, cfg.DbURL)
|
||||
psqlCmd.Stdin, psqlCmd.Stdout, psqlCmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
fmt.Println("Launching psql…")
|
||||
return psqlCmd.Run()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
dbCmd.AddCommand(dbPsqlCmd)
|
||||
|
||||
rootCmd.AddCommand(dbCmd)
|
||||
}
|
||||
110
cmd/serve.go
110
cmd/serve.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/glueops/autoglue/internal/auth"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -38,6 +39,8 @@ var serveCmd = &cobra.Command{
|
||||
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
|
||||
go func() {
|
||||
if err := jobs.Start(); err != nil {
|
||||
@@ -50,7 +53,7 @@ var serveCmd = &cobra.Command{
|
||||
{
|
||||
// schedule next 03:30 local time
|
||||
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(),
|
||||
uuid.NewString(),
|
||||
"archer_cleanup",
|
||||
@@ -58,10 +61,13 @@ var serveCmd = &cobra.Command{
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue archer cleanup job: %v", err)
|
||||
}
|
||||
|
||||
// schedule next 03:45 local time
|
||||
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(),
|
||||
uuid.NewString(),
|
||||
"tokens_cleanup",
|
||||
@@ -69,46 +75,47 @@ var serveCmd = &cobra.Command{
|
||||
archer.WithScheduleTime(next2),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
/*
|
||||
_, _ = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"tokens_cleanup",
|
||||
bg.TokensCleanupArgs{},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(10*time.Second)),
|
||||
)
|
||||
*/
|
||||
case <-schedCtx.Done():
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue token cleanup job: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"db_backup_s3",
|
||||
bg.DbBackupArgs{IntervalS: 3600},
|
||||
archer.WithMaxRetries(1),
|
||||
archer.WithScheduleTime(time.Now().Add(1*time.Hour)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue backup jobs: %v", err)
|
||||
}
|
||||
|
||||
_, 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)
|
||||
go func() {
|
||||
@@ -119,7 +126,26 @@ var serveCmd = &cobra.Command{
|
||||
}
|
||||
}()
|
||||
|
||||
r := api.NewRouter(rt.DB, jobs)
|
||||
r := api.NewRouter(rt.DB, jobs, nil)
|
||||
|
||||
if cfg.DBStudioEnabled {
|
||||
dbURL := cfg.DbURLRO
|
||||
if dbURL == "" {
|
||||
dbURL = cfg.DbURL
|
||||
}
|
||||
|
||||
studio, err := api.PgwebHandler(
|
||||
dbURL,
|
||||
"db-studio",
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init db studio: %v", err)
|
||||
} else {
|
||||
r = api.NewRouter(rt.DB, jobs, studio)
|
||||
log.Printf("pgweb mounted at /db-studio/")
|
||||
}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port)
|
||||
|
||||
|
||||
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
|
||||
- secret
|
||||
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:
|
||||
properties:
|
||||
key:
|
||||
@@ -175,6 +188,28 @@ definitions:
|
||||
- worker
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
bits:
|
||||
@@ -253,6 +288,27 @@ definitions:
|
||||
updated_at:
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
payload:
|
||||
@@ -440,6 +496,34 @@ definitions:
|
||||
example: 7
|
||||
type: integer
|
||||
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:
|
||||
properties:
|
||||
refresh_token:
|
||||
@@ -575,6 +659,23 @@ definitions:
|
||||
description: set if rotating
|
||||
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:
|
||||
properties:
|
||||
key:
|
||||
@@ -592,6 +693,30 @@ definitions:
|
||||
- worker
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
hostname:
|
||||
@@ -1812,6 +1937,406 @@ paths:
|
||||
summary: Reveal decrypted secret (one-time read)
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
@@ -3621,6 +4146,57 @@ paths:
|
||||
summary: Update server (org scoped)
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
67
go.mod
67
go.mod
@@ -4,8 +4,14 @@ go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
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/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/coreos/go-oidc/v3 v3.16.0
|
||||
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/cors v1.2.2
|
||||
github.com/go-chi/httprate v0.15.0
|
||||
@@ -14,11 +20,12 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sosedoff/pgweb v0.16.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc4
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/crypto v0.44.0
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/datatypes v1.2.7
|
||||
@@ -28,9 +35,32 @@ require (
|
||||
|
||||
require (
|
||||
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/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/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/endpoints/v2 v2.7.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // 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/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/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-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
@@ -41,23 +71,38 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // 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/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // 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/now v1.1.5 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // 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/lib/pq v1.10.9 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // 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/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/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
@@ -66,12 +111,20 @@ require (
|
||||
github.com/sv-tools/openapi v0.2.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // 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
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // 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/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.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
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
)
|
||||
|
||||
183
go.sum
183
go.sum
@@ -1,13 +1,89 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
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/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
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/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.19 h1:qdUtOw4JhZr2YcKO3g0ho/IcFXfXrrb8xlX05Y6EvSw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.19/go.mod h1:tMJ8bur01t8eEm0atLadkIIFA154OJ4JCKZeQ+o+R7k=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.23 h1:IQILcxVgMO2BVLaJ2aAv21dKWvE1MduNrbvuK43XL2Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.23/go.mod h1:JRodHszhVdh5TPUknxDzJzrMiznG+M+FfR3WSWKgCI8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
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/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/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.1 h1:kKJk9r6iLMfCGy8RL9GWg3n9gUE1IpSwqYP3/5bdL1s=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.1/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/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/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 h1:/p6MxkbQoCzaGQT3WO0JwG0FlQyG9RD8VmdmoKc5xqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.2/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6 h1:0dES42T2dhICCbVB3JSTTn7+Bz93wfJEK1b7jksZIyQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ=
|
||||
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.1 h1:5sbIM57lHLaEaNWdIx23JH30LNBsSDkjN/QXGcRLAFc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
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/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/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -16,6 +92,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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -24,6 +102,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/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/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/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
@@ -53,6 +135,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/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-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.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
@@ -60,6 +143,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/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-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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
@@ -67,8 +152,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/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -81,16 +167,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/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/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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
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/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -101,6 +193,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/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/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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -111,12 +204,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/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.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.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/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/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/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||
@@ -126,14 +230,30 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
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/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
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/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -153,6 +273,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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -172,38 +293,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/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
|
||||
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/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/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
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/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-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.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/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
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-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.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.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/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-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.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/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-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-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-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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -211,30 +350,32 @@ 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.12.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/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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
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.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.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.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/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-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.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/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -49,6 +49,14 @@ func AuthMiddleware(db *gorm.DB, requireOrg bool) func(http.Handler) http.Handle
|
||||
} else if appKey := r.Header.Get("X-APP-KEY"); appKey != "" {
|
||||
secret := r.Header.Get("X-APP-SECRET")
|
||||
user = auth.ValidateAppKeyPair(appKey, secret, db)
|
||||
} else if c, err := r.Cookie("ag_jwt"); err == nil {
|
||||
tok := strings.TrimSpace(c.Value)
|
||||
if strings.HasPrefix(strings.ToLower(tok), "bearer ") {
|
||||
tok = tok[7:]
|
||||
}
|
||||
if tok != "" {
|
||||
user = auth.ValidateJWT(tok, db)
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
|
||||
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
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
// 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'",
|
||||
}, "; "))
|
||||
} else {
|
||||
@@ -53,7 +53,7 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"connect-src 'self'",
|
||||
"connect-src 'self' ws://localhost:8080 https://api.github.com",
|
||||
"frame-ancestors 'none'",
|
||||
}, "; "))
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
httpPprof "net/http/pprof"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/docs"
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
@@ -23,11 +22,9 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||
)
|
||||
|
||||
func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
|
||||
func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
|
||||
l := log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"})
|
||||
@@ -59,187 +56,44 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
|
||||
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.Route("/api", func(api chi.Router) {
|
||||
api.Route("/v1", func(v1 chi.Router) {
|
||||
|
||||
// Versioned API
|
||||
mountAPIRoutes(r, db, jobs)
|
||||
|
||||
// Optional DB studio
|
||||
if studio != nil {
|
||||
r.Group(func(gr chi.Router) {
|
||||
authUser := httpmiddleware.AuthMiddleware(db, false)
|
||||
authOrg := httpmiddleware.AuthMiddleware(db, true)
|
||||
|
||||
// Also serving a versioned JWKS for swagger, which uses BasePath
|
||||
v1.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||
|
||||
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))
|
||||
})
|
||||
adminOnly := httpmiddleware.RequirePlatformAdmin()
|
||||
gr.Use(authUser, adminOnly)
|
||||
gr.Mount("/db-studio", studio)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pprof
|
||||
if config.IsDebug() {
|
||||
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"))
|
||||
})
|
||||
mountPprofRoutes(r)
|
||||
}
|
||||
|
||||
// Swagger
|
||||
if config.IsSwaggerEnabled() {
|
||||
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"))
|
||||
mountSwaggerRoutes(r)
|
||||
}
|
||||
|
||||
// UI dev/prod
|
||||
if config.IsUIDev() {
|
||||
fmt.Println("Running in development mode")
|
||||
// Dev: isolate proxy from chi middlewares so WS upgrade can hijack.
|
||||
proxy, err := web.DevProxy("http://localhost:5173")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dev proxy init failed")
|
||||
@@ -247,22 +101,20 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// Send API/Swagger/pprof to chi
|
||||
mux.Handle("/api/", r)
|
||||
mux.Handle("/api", r)
|
||||
mux.Handle("/swagger/", r)
|
||||
mux.Handle("/db-studio/", r)
|
||||
mux.Handle("/debug/pprof/", r)
|
||||
// Everything else (/, /brand-preview, assets) → proxy (no middlewares)
|
||||
mux.Handle("/", proxy)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
fmt.Println("Running in production mode")
|
||||
if h, err := web.SPAHandler(); err == nil {
|
||||
r.NotFound(h.ServeHTTP)
|
||||
} else {
|
||||
fmt.Println("Running in production mode")
|
||||
if h, err := web.SPAHandler(); err == nil {
|
||||
r.NotFound(h.ServeHTTP)
|
||||
} else {
|
||||
log.Error().Err(err).Msg("spa handler init failed")
|
||||
}
|
||||
log.Error().Err(err).Msg("spa handler init failed")
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
@@ -41,6 +41,8 @@ func NewRuntime() *Runtime {
|
||||
&models.NodePool{},
|
||||
&models.Cluster{},
|
||||
&models.Credential{},
|
||||
&models.Domain{},
|
||||
&models.RecordSet{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
264
internal/bg/backup_s3.go
Normal file
264
internal/bg/backup_s3.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package bg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/dyaksa/archer/job"
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DbBackupArgs struct {
|
||||
IntervalS int `json:"interval_seconds,omitempty"`
|
||||
}
|
||||
|
||||
type s3Scope struct {
|
||||
Service string `json:"service"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type encAWS struct {
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
SecretAccessKey string `json:"secret_access_key"`
|
||||
}
|
||||
|
||||
func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queue := j.QueueName
|
||||
if strings.TrimSpace(queue) == "" {
|
||||
queue = "db_backup_s3"
|
||||
}
|
||||
|
||||
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||
|
||||
payload := DbBackupArgs{}
|
||||
|
||||
opts := []archer.FnOptions{
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
}
|
||||
|
||||
if _, err := jobs.Enqueue(ctx, uuid.NewString(), queue, payload, opts...); err != nil {
|
||||
log.Error().Err(err).Str("queue", queue).Time("next", next).Msg("failed to enqueue next db backup")
|
||||
} else {
|
||||
log.Info().Str("queue", queue).Time("next", next).Msg("scheduled next db backup")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func DbBackup(ctx context.Context, db *gorm.DB) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
cred, sc, err := loadS3Credential(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load credential: %w", err)
|
||||
}
|
||||
|
||||
ak, sk, err := decryptAwsAccessKeys(ctx, db, cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt aws keys: %w", err)
|
||||
}
|
||||
|
||||
region := sc.Region
|
||||
|
||||
if strings.TrimSpace(region) == "" {
|
||||
region = cred.Region
|
||||
if strings.TrimSpace(region) == "" {
|
||||
region = "us-west-1"
|
||||
}
|
||||
}
|
||||
|
||||
bucket := strings.ToLower(fmt.Sprintf("%s-autoglue-backups-%s", cred.OrganizationID, region))
|
||||
|
||||
s3cli, err := makeS3Client(ctx, ak, sk, region)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureBucket(ctx, s3cli, bucket, region); err != nil {
|
||||
return fmt.Errorf("ensure bucket: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
now := time.Now().UTC()
|
||||
key := fmt.Sprintf("%04d/%02d/%02d/backup-%02d.sql", now.Year(), now.Month(), now.Day(), now.Hour())
|
||||
outPath := filepath.Join(tmpDir, "autoglue-backup-"+now.Format("20060102T150405Z")+".sql")
|
||||
|
||||
if err := runPgDump(ctx, cfg.DbURL, outPath); err != nil {
|
||||
return fmt.Errorf("pg_dump: %w", err)
|
||||
}
|
||||
defer os.Remove(outPath)
|
||||
|
||||
if err := uploadFileToS3(ctx, s3cli, bucket, key, outPath); err != nil {
|
||||
return fmt.Errorf("s3 upload: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("bucket", bucket).Str("key", key).Msg("backup uploaded")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Helpers
|
||||
|
||||
func loadS3Credential(ctx context.Context, db *gorm.DB) (models.Credential, s3Scope, error) {
|
||||
var c models.Credential
|
||||
err := db.
|
||||
WithContext(ctx).
|
||||
Where("provider = ? AND kind = ? AND scope_kind = ?", "aws", "aws_access_key", "service").
|
||||
Where("scope ->> 'service' = ?", "s3").
|
||||
Order("created_at DESC").
|
||||
First(&c).Error
|
||||
if err != nil {
|
||||
return models.Credential{}, s3Scope{}, fmt.Errorf("load credential: %w", err)
|
||||
}
|
||||
|
||||
var sc s3Scope
|
||||
_ = json.Unmarshal(c.Scope, &sc)
|
||||
return c, sc, nil
|
||||
}
|
||||
|
||||
func decryptAwsAccessKeys(ctx context.Context, db *gorm.DB, c models.Credential) (string, string, error) {
|
||||
plain, err := utils.DecryptForOrg(c.OrganizationID, c.EncryptedData, c.IV, c.Tag, db)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var payload encAWS
|
||||
if err := json.Unmarshal([]byte(plain), &payload); err != nil {
|
||||
return "", "", fmt.Errorf("parse decrypted payload: %w", err)
|
||||
}
|
||||
|
||||
if payload.AccessKeyID == "" || payload.SecretAccessKey == "" {
|
||||
return "", "", errors.New("decrypted payload missing keys")
|
||||
}
|
||||
return payload.AccessKeyID, payload.SecretAccessKey, nil
|
||||
}
|
||||
|
||||
func makeS3Client(ctx context.Context, accessKey, secret, region string) (*s3.Client, error) {
|
||||
staticCredentialsProvider := credentials.NewStaticCredentialsProvider(accessKey, secret, "")
|
||||
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithCredentialsProvider(staticCredentialsProvider), awsconfig.WithRegion(region))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aws config: %w", err)
|
||||
}
|
||||
return s3.NewFromConfig(cfg), nil
|
||||
}
|
||||
|
||||
func ensureBucket(ctx context.Context, s3cli *s3.Client, bucket, region string) error {
|
||||
_, err := s3cli.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucket)})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if out, err := s3cli.GetBucketLocation(ctx, &s3.GetBucketLocationInput{Bucket: aws.String(bucket)}); err == nil {
|
||||
existing := string(out.LocationConstraint)
|
||||
if existing == "" {
|
||||
existing = "us-east-1"
|
||||
}
|
||||
if existing != region {
|
||||
return fmt.Errorf("bucket %q already exists in region %q (requested %q)", bucket, existing, region)
|
||||
}
|
||||
}
|
||||
|
||||
// Create; LocationConstraint except us-east-1
|
||||
in := &s3.CreateBucketInput{Bucket: aws.String(bucket)}
|
||||
if region != "us-east-1" {
|
||||
in.CreateBucketConfiguration = &s3types.CreateBucketConfiguration{
|
||||
LocationConstraint: s3types.BucketLocationConstraint(region),
|
||||
}
|
||||
}
|
||||
if _, err := s3cli.CreateBucket(ctx, in); err != nil {
|
||||
return fmt.Errorf("create bucket: %w", err)
|
||||
}
|
||||
|
||||
// default SSE (best-effort)
|
||||
_, _ = s3cli.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
|
||||
Bucket: aws.String(bucket),
|
||||
ServerSideEncryptionConfiguration: &s3types.ServerSideEncryptionConfiguration{
|
||||
Rules: []s3types.ServerSideEncryptionRule{
|
||||
{ApplyServerSideEncryptionByDefault: &s3types.ServerSideEncryptionByDefault{
|
||||
SSEAlgorithm: s3types.ServerSideEncryptionAes256,
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func runPgDump(ctx context.Context, dbURL, outPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-owner",
|
||||
"--no-privileges",
|
||||
"--format=plain",
|
||||
"--file", outPath,
|
||||
dbURL,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "pg_dump", args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("pg_dump failed: %v | %s", err, stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadFileToS3(ctx context.Context, s3cli *s3.Client, bucket, key, path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
info, _ := f.Stat()
|
||||
_, err = s3cli.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: f,
|
||||
ContentLength: aws.Int64(info.Size()),
|
||||
ContentType: aws.String(mime.TypeByExtension(filepath.Ext(path))),
|
||||
ServerSideEncryption: s3types.ServerSideEncryptionAes256,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package bg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,13 +13,16 @@ import (
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ----- Public types -----
|
||||
|
||||
type BastionBootstrapArgs struct{}
|
||||
type BastionBootstrapArgs struct {
|
||||
IntervalS int `json:"interval_seconds,omitempty"`
|
||||
}
|
||||
|
||||
type BastionBootstrapFailure struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
@@ -39,11 +42,17 @@ type BastionBootstrapResult struct {
|
||||
|
||||
// ----- 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) {
|
||||
args := BastionBootstrapArgs{IntervalS: 120}
|
||||
jobID := j.ID
|
||||
start := time.Now()
|
||||
|
||||
_ = j.ParseArguments(&args)
|
||||
if args.IntervalS <= 0 {
|
||||
args.IntervalS = 120
|
||||
}
|
||||
|
||||
var servers []models.Server
|
||||
if err := db.
|
||||
Preload("SshKey").
|
||||
@@ -105,7 +114,7 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
||||
// 4) SSH + install docker
|
||||
host := net.JoinHostPort(*s.PublicIPAddress, "22")
|
||||
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()
|
||||
|
||||
if err != nil {
|
||||
@@ -147,9 +156,17 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
||||
Failures: failures,
|
||||
}
|
||||
|
||||
// log.Printf("[bastion] level=INFO job=%s step=finish processed=%d ready=%d failed=%d elapsed_ms=%d",
|
||||
// jobID, proc, ok, fail, res.ElapsedMs)
|
||||
log.Debug().Int("processed", proc).Int("ready", ok).Int("failed", fail).Msg("[bastion] reconcile tick ok")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -187,16 +204,24 @@ func logHostInfo(jobID string, s *models.Server, step, msg string, kv ...any) {
|
||||
// ----- SSH & command execution -----
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
|
||||
hkcb := makeDBHostKeyCallback(db, s)
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: known_hosts verification
|
||||
HostKeyCallback: hkcb,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
@@ -494,3 +519,38 @@ func wrapSSHError(err error, output string) error {
|
||||
func sshEscape(s string) string {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
archer.WithSetTableName("jobs"), // <- ensure correct table
|
||||
archer.WithSleepInterval(1*time.Second), // fast poll while debugging
|
||||
archer.WithErrHandler(func(err error) { // bubble up worker SQL errors
|
||||
log.Printf("[archer] ERROR: %v", err)
|
||||
log.Error().Err(err).Msg("[archer] worker error")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
|
||||
c.Register(
|
||||
"bootstrap_bastion",
|
||||
BastionBootstrapWorker(gdb),
|
||||
BastionBootstrapWorker(gdb, jobs),
|
||||
archer.WithInstances(instances),
|
||||
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
||||
)
|
||||
@@ -94,6 +94,19 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
archer.WithTimeout(5*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"db_backup_s3",
|
||||
DbBackupWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(15*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"dns_reconcile",
|
||||
DNSReconsileWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
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)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type Config struct {
|
||||
DbURL string
|
||||
DbURLRO string
|
||||
Port string
|
||||
Host string
|
||||
JWTIssuer string
|
||||
@@ -29,6 +30,12 @@ type Config struct {
|
||||
Debug bool
|
||||
Swagger bool
|
||||
SwaggerHost string
|
||||
|
||||
DBStudioEnabled bool
|
||||
DBStudioBind string
|
||||
DBStudioPort string
|
||||
DBStudioUser string
|
||||
DBStudioPass string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -48,6 +55,12 @@ func Load() (Config, error) {
|
||||
v.SetDefault("bind.address", "127.0.0.1")
|
||||
v.SetDefault("bind.port", "8080")
|
||||
v.SetDefault("database.url", "postgres://user:pass@localhost:5432/db?sslmode=disable")
|
||||
v.SetDefault("database.url_ro", "")
|
||||
v.SetDefault("db_studio.enabled", false)
|
||||
v.SetDefault("db_studio.bind", "127.0.0.1")
|
||||
v.SetDefault("db_studio.port", "0") // 0 = random
|
||||
v.SetDefault("db_studio.user", "")
|
||||
v.SetDefault("db_studio.pass", "")
|
||||
|
||||
v.SetDefault("ui.dev", false)
|
||||
v.SetDefault("env", "development")
|
||||
@@ -63,6 +76,7 @@ func Load() (Config, error) {
|
||||
"bind.address",
|
||||
"bind.port",
|
||||
"database.url",
|
||||
"database.url_ro",
|
||||
"jwt.issuer",
|
||||
"jwt.audience",
|
||||
"jwt.private.enc.key",
|
||||
@@ -76,6 +90,11 @@ func Load() (Config, error) {
|
||||
"debug",
|
||||
"swagger",
|
||||
"swagger.host",
|
||||
"db_studio.enabled",
|
||||
"db_studio.bind",
|
||||
"db_studio.port",
|
||||
"db_studio.user",
|
||||
"db_studio.pass",
|
||||
}
|
||||
for _, k := range keys {
|
||||
_ = v.BindEnv(k)
|
||||
@@ -84,6 +103,7 @@ func Load() (Config, error) {
|
||||
// Build config
|
||||
cfg := Config{
|
||||
DbURL: v.GetString("database.url"),
|
||||
DbURLRO: v.GetString("database.url_ro"),
|
||||
Port: v.GetString("bind.port"),
|
||||
Host: v.GetString("bind.address"),
|
||||
JWTIssuer: v.GetString("jwt.issuer"),
|
||||
@@ -100,6 +120,12 @@ func Load() (Config, error) {
|
||||
Debug: v.GetBool("debug"),
|
||||
Swagger: v.GetBool("swagger"),
|
||||
SwaggerHost: v.GetString("swagger.host"),
|
||||
|
||||
DBStudioEnabled: v.GetBool("db_studio.enabled"),
|
||||
DBStudioBind: v.GetString("db_studio.bind"),
|
||||
DBStudioPort: v.GetString("db_studio.port"),
|
||||
DBStudioUser: v.GetString("db_studio.user"),
|
||||
DBStudioPass: v.GetString("db_studio.pass"),
|
||||
}
|
||||
|
||||
// Validate
|
||||
|
||||
@@ -273,6 +273,21 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
||||
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()),
|
||||
})
|
||||
|
||||
// If the state indicates SPA popup mode, postMessage tokens to the opener and close
|
||||
state := r.URL.Query().Get("state")
|
||||
if strings.Contains(state, "mode=spa") {
|
||||
@@ -356,6 +371,21 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
||||
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{
|
||||
AccessToken: access,
|
||||
RefreshToken: newPair.Plain,
|
||||
@@ -377,6 +407,7 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
||||
// @Router /auth/logout [post]
|
||||
func Logout(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, _ := config.Load()
|
||||
var req dto.LogoutRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||
@@ -385,13 +416,27 @@ func Logout(db *gorm.DB) http.HandlerFunc {
|
||||
rec, err := auth.ValidateRefreshToken(db, req.RefreshToken)
|
||||
if err != nil {
|
||||
w.WriteHeader(204) // already invalid/revoked
|
||||
return
|
||||
goto clearCookie
|
||||
}
|
||||
if err := auth.RevokeFamily(db, rec.FamilyID); err != nil {
|
||||
utils.WriteError(w, 500, "revoke_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
clearCookie:
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ag_jwt",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
Expires: time.Unix(0, 0),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: strings.HasPrefix(cfg.OAuthRedirectBase, "https"),
|
||||
})
|
||||
|
||||
w.WriteHeader(204)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,20 +106,20 @@ func clusterToDTO(c models.Cluster) dto.ClusterResponse {
|
||||
}
|
||||
|
||||
return dto.ClusterResponse{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Provider: c.Provider,
|
||||
Region: c.Region,
|
||||
Status: c.Status,
|
||||
CaptainDomain: c.CaptainDomain,
|
||||
ClusterLoadBalancer: c.ClusterLoadBalancer,
|
||||
RandomToken: c.RandomToken,
|
||||
CertificateKey: c.CertificateKey,
|
||||
ControlLoadBalancer: c.ControlLoadBalancer,
|
||||
NodePools: nps,
|
||||
BastionServer: bastion,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Provider: c.Provider,
|
||||
Region: c.Region,
|
||||
Status: c.Status,
|
||||
CaptainDomain: c.CaptainDomain,
|
||||
//ClusterLoadBalancer: c.ClusterLoadBalancer,
|
||||
RandomToken: c.RandomToken,
|
||||
CertificateKey: c.CertificateKey,
|
||||
//ControlLoadBalancer: c.ControlLoadBalancer,
|
||||
NodePools: nps,
|
||||
BastionServer: bastion,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 ---
|
||||
|
||||
func validStatus(status string) bool {
|
||||
|
||||
@@ -6,15 +6,13 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Dns struct {
|
||||
type Backup struct {
|
||||
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_credential,priority:1"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
ClusterID *uuid.UUID `gorm:"type:uuid" json:"cluster_id,omitempty"`
|
||||
Cluster *Cluster `gorm:"foreignKey:ClusterID" json:"cluster,omitempty"`
|
||||
Type string `gorm:"not null" json:"type,omitempty"`
|
||||
Name string `gorm:"not null" json:"name,omitempty"`
|
||||
Content string `gorm:"not null" json:"content,omitempty"`
|
||||
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()"`
|
||||
}
|
||||
@@ -14,17 +14,20 @@ type Cluster struct {
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `gorm:"not null" json:"captain_domain"`
|
||||
ClusterLoadBalancer string `json:"cluster_load_balancer"`
|
||||
ControlLoadBalancer string `json:"control_load_balancer"`
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||
KubeIV string `json:"-"`
|
||||
KubeTag string `json:"-"`
|
||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,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()"`
|
||||
CaptainDomain string `gorm:"not null" json:"captain_domain"` // nonprod.earth.onglueops.rocks
|
||||
AppsLoadBalancer string `json:"cluster_load_balancer"` // {public_ip: 1.2.3.4, private_ip: 10.0.30.1, name: apps.CaqptainDomain}
|
||||
GlueOpsLoadBalancer string `json:"control_load_balancer"` // {public_ip: 5.6.7.8, private_ip: 10.0.22.1, name: CaptainDomain}
|
||||
|
||||
ControlPlane string `json:"control_plane"` // <- dns cntlpn
|
||||
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||
KubeIV string `json:"-"`
|
||||
KubeTag string `json:"-"`
|
||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,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()"`
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type Credential struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_provider_scopekind_scope,priority:1" json:"organization_id"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
Provider string `gorm:"type:varchar(50);not null;uniqueIndex:uniq_org_provider_scopekind_scope,priority:2;index:idx_provider_kind"`
|
||||
Kind string `gorm:"type:varchar(50);not null;index:idx_provider_kind;index:idx_kind_scope"`
|
||||
|
||||
41
internal/models/domain.go
Normal file
41
internal/models/domain.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
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"`
|
||||
DomainName string `gorm:"type:varchar(253);not null;uniqueIndex:uniq_org_domain,priority:2"`
|
||||
ZoneID string `gorm:"type:varchar(128);not null;default:''"` // backfilled for R53 (e.g. "/hostedzone/Z123...")
|
||||
Status string `gorm:"type:varchar(20);not null;default:'pending'"` // pending, provisioning, ready, failed
|
||||
LastError string `gorm:"type:text;not null;default:''"`
|
||||
CredentialID uuid.UUID `gorm:"type:uuid;not null" 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()"`
|
||||
}
|
||||
|
||||
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"
|
||||
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"`
|
||||
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"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ func SPAHandler() (http.Handler, error) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") ||
|
||||
r.URL.Path == "/api" ||
|
||||
strings.HasPrefix(r.URL.Path, "/swagger") ||
|
||||
strings.HasPrefix(r.URL.Path, "/db-studio") ||
|
||||
strings.HasPrefix(r.URL.Path, "/debug/pprof") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM postgres:17.6@sha256:00bc86618629af00d2937fdc5a5d63db3ff8450acf52f0636ec813c7f4902929
|
||||
FROM postgres:17.7@sha256:44640f16641cf36716cabd011e2f7eb4742b6b6b19f4488ddcbb7c250e5c9753
|
||||
|
||||
RUN cd /var/lib/postgresql/ && \
|
||||
openssl req -new -text -passout pass:abcd -subj /CN=localhost -out server.req -keyout privkey.pem && \
|
||||
|
||||
137
tools/pgweb_fetch.go
Normal file
137
tools/pgweb_fetch.go
Normal file
@@ -0,0 +1,137 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
Name string
|
||||
URL string
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
const version = "0.16.2"
|
||||
|
||||
func main() {
|
||||
targets := []Target{
|
||||
{
|
||||
Name: "pgweb-linux-amd64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_amd64.zip", version),
|
||||
SHA256: "3d6c2063e1040b8a625eb7c43c9b84f8ed12cfc9a798eacbce85179963ee2554",
|
||||
},
|
||||
{
|
||||
Name: "pgweb-linux-arm64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_arm64.zip", version),
|
||||
SHA256: "079c698a323ed6431ce7e6343ee5847c7da62afbf45dfb2e78f8289d7b381783",
|
||||
},
|
||||
{
|
||||
Name: "pgweb-darwin-amd64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_amd64.zip", version),
|
||||
SHA256: "c0a098e2eb9cf9f7c20161a2947522eb67eacbf2b6c3389c2f8e8c5ed7238957",
|
||||
},
|
||||
{
|
||||
Name: "pgweb-darwin-arm64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_arm64.zip", version),
|
||||
SHA256: "c8f5fca847f461ba22a619e2d96cb1656cefdffd8f2aef2340e14fc5b518d3a2",
|
||||
},
|
||||
}
|
||||
|
||||
outDir := filepath.Join("internal", "web", "pgwebbin")
|
||||
_ = os.MkdirAll(outDir, 0o755)
|
||||
|
||||
for _, t := range targets {
|
||||
destZip := filepath.Join(outDir, t.Name+".zip")
|
||||
fmt.Printf("Downloading %s...\n", t.URL)
|
||||
if err := downloadFile(destZip, t.URL); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
binPath := filepath.Join(outDir, t.Name)
|
||||
if err := unzipSingle(destZip, binPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = os.Remove(destZip)
|
||||
|
||||
// Make executable
|
||||
if err := os.Chmod(binPath, 0o755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Saved %s\n", binPath)
|
||||
|
||||
// Compute checksum
|
||||
sum, _ := fileSHA256(binPath)
|
||||
fmt.Printf(" SHA256: %s\n", sum)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(dest, url string) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func unzipSingle(zipPath, outPath string) error {
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
out, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -77,7 +77,7 @@
|
||||
"shadcn": "3.5.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.3",
|
||||
"typescript-eslint": "8.46.4",
|
||||
"vite": "7.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ProtectedRoute } from "@/components/protected-route.tsx"
|
||||
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
||||
import { Login } from "@/pages/auth/login.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 { LabelsPage } from "@/pages/labels/labels-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="/node-pools" element={<NodePoolsPage />} />
|
||||
<Route path="/credentials" element={<CredentialPage />} />
|
||||
<Route path="/dns" element={<DnsPage />} />
|
||||
|
||||
<Route path="/admin/jobs" element={<JobsPage />} />
|
||||
</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"
|
||||
import { AiOutlineCluster } from "react-icons/ai"
|
||||
import { GrUserWorker } from "react-icons/gr"
|
||||
import { MdOutlineDns } from "react-icons/md"
|
||||
|
||||
export type NavItem = {
|
||||
to: string
|
||||
@@ -23,6 +24,7 @@ export type NavItem = {
|
||||
|
||||
export const mainNav: NavItem[] = [
|
||||
{ to: "/clusters", label: "Clusters", icon: AiOutlineCluster },
|
||||
{ to: "/dns", label: "DNS", icon: MdOutlineDns },
|
||||
{ to: "/node-pools", label: "Node Pools", icon: BoxesIcon },
|
||||
{ to: "/annotations", label: "Annotations", icon: ComponentIcon },
|
||||
{ to: "/labels", label: "Labels", icon: TagsIcon },
|
||||
|
||||
@@ -2,7 +2,16 @@ import { useMemo, useState } from "react"
|
||||
import { credentialsApi } from "@/api/credentials"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
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 { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
@@ -20,16 +29,36 @@ import {
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} 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 { 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 { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
@@ -1352,6 +1381,7 @@ export const CredentialPage = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<pre>{JSON.stringify(credentialQ.data, null, 2)}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 { orgStore } from "@/auth/org.ts"
|
||||
import type { DtoCredentialOut } from "@/sdk"
|
||||
import { makeOrgsApi } from "@/sdkClient.ts"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
@@ -20,6 +22,20 @@ import {
|
||||
} from "@/components/ui/form.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({
|
||||
name: z.string().min(1, "Required"),
|
||||
domain: z.string().optional(),
|
||||
@@ -38,6 +54,13 @@ export const OrgSettings = () => {
|
||||
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>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AuthApi,
|
||||
Configuration,
|
||||
CredentialsApi,
|
||||
DNSApi,
|
||||
LabelsApi,
|
||||
MeApi,
|
||||
MeAPIKeysApi,
|
||||
@@ -118,3 +119,7 @@ export function makeMetaApi() {
|
||||
export function makeCredentialsApi() {
|
||||
return makeApiClient(CredentialsApi)
|
||||
}
|
||||
|
||||
export function makeDnsApi() {
|
||||
return makeApiClient(DNSApi)
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
import path from "path"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
}
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8080",
|
||||
"/swagger": "http://localhost:8080",
|
||||
},
|
||||
allowedHosts: ['.getexposed.io']
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8080",
|
||||
"/swagger": "http://localhost:8080",
|
||||
"/db-studio": "http://localhost:8080",
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000,
|
||||
outDir: "../internal/web/dist",
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
cssMinify: "lightningcss",
|
||||
rollupOptions: { output: { manualChunks: { react: ["react","react-dom","react-router-dom"] } } }
|
||||
allowedHosts: [".getexposed.io"],
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000,
|
||||
outDir: "../internal/web/dist",
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
cssMinify: "lightningcss",
|
||||
rollupOptions: {
|
||||
output: { manualChunks: { react: ["react", "react-dom", "react-router-dom"] } },
|
||||
},
|
||||
esbuild: { legalComments: "none" }
|
||||
},
|
||||
esbuild: { legalComments: "none" },
|
||||
})
|
||||
|
||||
252
ui/yarn.lock
252
ui/yarn.lock
@@ -590,23 +590,23 @@
|
||||
integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==
|
||||
|
||||
"@inquirer/confirm@^5.0.0":
|
||||
version "5.1.20"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.20.tgz#8e85662584f162b8b9f6a7c9edcb430fd79f56ad"
|
||||
integrity sha512-HDGiWh2tyRZa0M1ZnEIUCQro25gW/mN8ODByicQrbR1yHx4hT+IOpozCMi5TgBtUdklLwRI2mv14eNpftDluEw==
|
||||
version "5.1.21"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.21.tgz#610c4acd7797d94890a6e2dde2c98eb1e891dd12"
|
||||
integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==
|
||||
dependencies:
|
||||
"@inquirer/core" "^10.3.1"
|
||||
"@inquirer/core" "^10.3.2"
|
||||
"@inquirer/type" "^3.0.10"
|
||||
|
||||
"@inquirer/core@^10.3.1":
|
||||
version "10.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.1.tgz#09bba1c6e0c45cfd3975c0c85c784c61b916baa8"
|
||||
integrity sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==
|
||||
"@inquirer/core@^10.3.2":
|
||||
version "10.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.2.tgz#535979ff3ff4fe1e7cc4f83e2320504c743b7e20"
|
||||
integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^1.0.2"
|
||||
"@inquirer/figures" "^1.0.15"
|
||||
"@inquirer/type" "^3.0.10"
|
||||
cli-width "^4.1.0"
|
||||
mute-stream "^3.0.0"
|
||||
mute-stream "^2.0.0"
|
||||
signal-exit "^4.1.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
yoctocolors-cjs "^2.1.3"
|
||||
@@ -668,9 +668,9 @@
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@modelcontextprotocol/sdk@^1.17.2":
|
||||
version "1.21.1"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.21.1.tgz#8fba02e7581d49cc9b047aab0cfd334043321fe5"
|
||||
integrity sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz#b982143dfd36ef096b311d8ffadb2f91aabbbb1d"
|
||||
integrity sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==
|
||||
dependencies:
|
||||
ajv "^8.17.1"
|
||||
ajv-formats "^3.0.1"
|
||||
@@ -1602,17 +1602,17 @@
|
||||
"@tailwindcss/oxide" "4.1.17"
|
||||
tailwindcss "4.1.17"
|
||||
|
||||
"@tanstack/query-core@5.90.7":
|
||||
version "5.90.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.7.tgz#ebe0d692e83bd76bde36a371955b0944a204f10c"
|
||||
integrity sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==
|
||||
"@tanstack/query-core@5.90.8":
|
||||
version "5.90.8"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.8.tgz#4a2600a5acf403494e4d5a3fcfac038ed84c5db7"
|
||||
integrity sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ==
|
||||
|
||||
"@tanstack/react-query@^5.90.7":
|
||||
version "5.90.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.7.tgz#68a32e9fc2bf8fa8c7890177f5ec35653731a1dd"
|
||||
integrity sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==
|
||||
version "5.90.8"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.8.tgz#c9b8bdbffd2df4b023094c3da144324a9daa5cef"
|
||||
integrity sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.90.7"
|
||||
"@tanstack/query-core" "5.90.8"
|
||||
|
||||
"@ts-morph/common@~0.27.0":
|
||||
version "0.27.0"
|
||||
@@ -1748,79 +1748,79 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa"
|
||||
integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz#6f7aeaf9f5c611425db9b8f983e8d3fe5deece3c"
|
||||
integrity sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==
|
||||
"@typescript-eslint/eslint-plugin@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz#005dc4eebcb27462f20de3afe888065f65cec100"
|
||||
integrity sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.3"
|
||||
"@typescript-eslint/type-utils" "8.46.3"
|
||||
"@typescript-eslint/utils" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/scope-manager" "8.46.4"
|
||||
"@typescript-eslint/type-utils" "8.46.4"
|
||||
"@typescript-eslint/utils" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.3.tgz#3badfb62d2e2dc733d02a038073e3f65f2cb833d"
|
||||
integrity sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==
|
||||
"@typescript-eslint/parser@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.4.tgz#1a5bfd48be57bc07eec64e090ac46e89f47ade31"
|
||||
integrity sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/scope-manager" "8.46.4"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.3.tgz#4555c685407ea829081218fa033d7b032607aaef"
|
||||
integrity sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==
|
||||
"@typescript-eslint/project-service@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.4.tgz#fa9872673b51fb57e5d5da034edbe17424ddd185"
|
||||
integrity sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.46.3"
|
||||
"@typescript-eslint/types" "^8.46.3"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.46.4"
|
||||
"@typescript-eslint/types" "^8.46.4"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz#2e330f566e135ccac13477b98dd88d8f176e4dff"
|
||||
integrity sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==
|
||||
"@typescript-eslint/scope-manager@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz#78c9b4856c0094def64ffa53ea955b46bec13304"
|
||||
integrity sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.46.3", "@typescript-eslint/tsconfig-utils@^8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz#cad33398c762c97fe56a8defda00c16505abefa3"
|
||||
integrity sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==
|
||||
"@typescript-eslint/tsconfig-utils@8.46.4", "@typescript-eslint/tsconfig-utils@^8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz#989a338093b6b91b0552f1f51331d89ec6980382"
|
||||
integrity sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==
|
||||
|
||||
"@typescript-eslint/type-utils@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz#71188df833d7697ecff256cd1d3889a20552d78c"
|
||||
integrity sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==
|
||||
"@typescript-eslint/type-utils@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz#ae71b428a3c138b5084affe47893c129949171e0"
|
||||
integrity sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/utils" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
"@typescript-eslint/utils" "8.46.4"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.46.3", "@typescript-eslint/types@^8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.3.tgz#da05ea40e91359b4275dbb3a489f2f7907a02245"
|
||||
integrity sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==
|
||||
"@typescript-eslint/types@8.46.4", "@typescript-eslint/types@^8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.4.tgz#38022bfda051be80e4120eeefcd2b6e3e630a69b"
|
||||
integrity sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz#c12406afba707f9779ce0c0151a08c33b3a96d41"
|
||||
integrity sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==
|
||||
"@typescript-eslint/typescript-estree@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz#6a9eeab0da45bf400f22c818e0f47102a980ceaa"
|
||||
integrity sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.46.3"
|
||||
"@typescript-eslint/tsconfig-utils" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/project-service" "8.46.4"
|
||||
"@typescript-eslint/tsconfig-utils" "8.46.4"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -1828,22 +1828,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.3.tgz#b6c7994b7c1ee2fe338ab32f7b3d4424856a73ce"
|
||||
integrity sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==
|
||||
"@typescript-eslint/utils@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.4.tgz#ea7878ddd625948cad4424dc2752b1be236556f5"
|
||||
integrity sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/scope-manager" "8.46.4"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz#6811b15053501981059c58e1c01b39242bd5c0f6"
|
||||
integrity sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==
|
||||
"@typescript-eslint/visitor-keys@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz#07031bd8d3ca6474e121221dae1055daead888f1"
|
||||
integrity sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@vitejs/plugin-react@5.1.0":
|
||||
@@ -1954,10 +1954,10 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
baseline-browser-mapping@^2.8.19:
|
||||
version "2.8.25"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz#947dc6f81778e0fa0424a2ab9ea09a3033e71109"
|
||||
integrity sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==
|
||||
baseline-browser-mapping@^2.8.25:
|
||||
version "2.8.28"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz#9ef511f5a7c19d74a94cafcbf951608398e9bdb3"
|
||||
integrity sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==
|
||||
|
||||
body-parser@^2.2.0:
|
||||
version "2.2.0"
|
||||
@@ -1997,14 +1997,14 @@ braces@^3.0.3:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.24.0, browserslist@^4.26.2:
|
||||
version "4.27.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.27.0.tgz#755654744feae978fbb123718b2f139bc0fa6697"
|
||||
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929"
|
||||
integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
|
||||
dependencies:
|
||||
baseline-browser-mapping "^2.8.19"
|
||||
caniuse-lite "^1.0.30001751"
|
||||
electron-to-chromium "^1.5.238"
|
||||
node-releases "^2.0.26"
|
||||
baseline-browser-mapping "^2.8.25"
|
||||
caniuse-lite "^1.0.30001754"
|
||||
electron-to-chromium "^1.5.249"
|
||||
node-releases "^2.0.27"
|
||||
update-browserslist-db "^1.1.4"
|
||||
|
||||
bytes@3.1.2, bytes@^3.1.2:
|
||||
@@ -2033,7 +2033,7 @@ callsites@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
caniuse-lite@^1.0.30001751:
|
||||
caniuse-lite@^1.0.30001754:
|
||||
version "1.0.30001754"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz#7758299d9a72cce4e6b038788a15b12b44002759"
|
||||
integrity sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==
|
||||
@@ -2365,10 +2365,10 @@ ee-first@1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
electron-to-chromium@^1.5.238:
|
||||
version "1.5.249"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz#e4fc3a3e60bb347361e4e876bb31903a9132a447"
|
||||
integrity sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==
|
||||
electron-to-chromium@^1.5.249:
|
||||
version "1.5.251"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.251.tgz#8168db0cb8fcc520bd4e5be720f2d8e0e4f7fe74"
|
||||
integrity sha512-lmyEOp4G0XT3qrYswNB4np1kx90k6QCXpnSHYv2xEsUuEu8JCobpDVYO6vMseirQyyCC6GCIGGxd5szMBa0tRA==
|
||||
|
||||
embla-carousel-react@^8.6.0:
|
||||
version "8.6.0"
|
||||
@@ -2698,9 +2698,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-equals@^5.0.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.2.tgz#75a9c7b1c2f627851349a2db94327d79b774ce83"
|
||||
integrity sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.3.tgz#e55f96198269278533348c22f1ab1a0fb957e22a"
|
||||
integrity sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==
|
||||
|
||||
fast-glob@^3.3.2, fast-glob@^3.3.3:
|
||||
version "3.3.3"
|
||||
@@ -3172,9 +3172,9 @@ jiti@^2.6.1:
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -3487,10 +3487,10 @@ msw@^2.10.4:
|
||||
until-async "^3.0.2"
|
||||
yargs "^17.7.2"
|
||||
|
||||
mute-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-3.0.0.tgz#cd8014dd2acb72e1e91bb67c74f0019e620ba2d1"
|
||||
integrity sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==
|
||||
mute-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b"
|
||||
integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==
|
||||
|
||||
nanoid@^3.3.11:
|
||||
version "3.3.11"
|
||||
@@ -3526,7 +3526,7 @@ node-fetch@^3.3.2:
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
node-releases@^2.0.26:
|
||||
node-releases@^2.0.27:
|
||||
version "2.0.27"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||
@@ -3871,16 +3871,16 @@ react-resizable-panels@^3.0.6:
|
||||
integrity sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==
|
||||
|
||||
react-router-dom@^7.9.5:
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.5.tgz#99a88cde83919bdfc84fbb3d6bf7c6fc18ca0758"
|
||||
integrity sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.6.tgz#f2a0d12961d67bd87ab48e5ef42fa1f45beae357"
|
||||
integrity sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==
|
||||
dependencies:
|
||||
react-router "7.9.5"
|
||||
react-router "7.9.6"
|
||||
|
||||
react-router@7.9.5:
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.5.tgz#68722186b4c9f42be36e658d9fe5d62ac1e0808b"
|
||||
integrity sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==
|
||||
react-router@7.9.6:
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.6.tgz#003c8de335fdd7390286a478dcfd9579c1826137"
|
||||
integrity sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==
|
||||
dependencies:
|
||||
cookie "^1.0.1"
|
||||
set-cookie-parser "^2.6.0"
|
||||
@@ -4416,15 +4416,15 @@ type-is@^2.0.0, type-is@^2.0.1:
|
||||
media-typer "^1.1.0"
|
||||
mime-types "^3.0.0"
|
||||
|
||||
typescript-eslint@8.46.3:
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.3.tgz#d58b337e4c6083ddef9a06542a03768a0150c564"
|
||||
integrity sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==
|
||||
typescript-eslint@8.46.4:
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.4.tgz#73cb93b5ff16e8627217240eeb0caf6e74e0a9fc"
|
||||
integrity sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.46.3"
|
||||
"@typescript-eslint/parser" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/utils" "8.46.3"
|
||||
"@typescript-eslint/eslint-plugin" "8.46.4"
|
||||
"@typescript-eslint/parser" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
"@typescript-eslint/utils" "8.46.4"
|
||||
|
||||
typescript@5.9.3:
|
||||
version "5.9.3"
|
||||
|
||||
Reference in New Issue
Block a user