mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6611dc4950 | ||
|
|
49665ffc9c | ||
|
|
ac14ef8fff | ||
|
|
f8e543b595 | ||
|
|
8cc81e52b7 | ||
|
|
d6e28c7fa2 | ||
|
|
9832229194 | ||
|
|
da82998754 | ||
|
|
bca32fe784 | ||
|
|
848e8d5179 | ||
|
|
d3ee38881c | ||
|
|
d39db44aa7 | ||
|
|
01b29a4706 | ||
|
|
bc3bd92d54 | ||
|
|
2057f92b82 | ||
|
|
169283b6c7 | ||
|
|
865270312c | ||
|
|
7cc447c0f5 | ||
|
|
8a0345f7f5 | ||
|
|
bb7114efe9 | ||
|
|
9dd0148764 | ||
|
|
bcc69e1c86 | ||
|
|
a7bf6b43b4 | ||
|
|
ced0a0663f | ||
|
|
dac28d3ea5 | ||
|
|
dd0cefc08a | ||
|
|
842f7c9be6 | ||
|
|
c15311a5a1 | ||
|
|
25ced343c4 | ||
|
|
b72a8d384d | ||
|
|
c786a79b60 | ||
|
|
01b1434842 | ||
|
|
e8c9cde474 | ||
|
|
ae92d05cd4 | ||
|
|
67d50d2b15 | ||
|
|
e5a664b812 | ||
|
|
f722ba8dca | ||
|
|
20e6d8d186 | ||
|
|
85f37cd113 | ||
|
|
fd1a81ecd8 | ||
|
|
793daf3ac3 | ||
|
|
7bef4ef6f1 | ||
|
|
9fa9cd169b | ||
|
|
8812b43346 | ||
|
|
21a6d7d5a1 | ||
|
|
da332c89dd | ||
|
|
fd25825f34 | ||
|
|
de3740e974 | ||
|
|
21dd26503f | ||
|
|
e1da229c30 | ||
|
|
5377e521e9 | ||
|
|
a929561bc8 | ||
|
|
c63f9f1cf3 | ||
|
|
4c02179b70 | ||
|
|
c8289c6936 | ||
|
|
c17caf22a2 | ||
|
|
986eeb9bf9 | ||
|
|
b0bbc13946 | ||
|
|
f50dcae823 | ||
|
|
ab9a77e1f5 | ||
|
|
416b2ff4e2 | ||
|
|
20bef7545c | ||
|
|
15e101439b | ||
|
|
fb0901a812 | ||
|
|
fee4c64551 | ||
|
|
4d37a6363f | ||
|
|
1dbdd04808 | ||
|
|
45b55015ac | ||
|
|
6b191089a5 | ||
|
|
d2e6ff9812 | ||
|
|
98a6cf7e51 | ||
|
|
fb4af74e3c | ||
|
|
1021e06655 | ||
|
|
c6be7bf8eb | ||
|
|
1429c40b2b | ||
|
|
73c4904a42 | ||
|
|
40df22c166 | ||
|
|
500a8d1095 | ||
|
|
eff69ff4ce | ||
|
|
2cd6ee91eb | ||
|
|
2d3800b576 | ||
|
|
eb86f2ad3c | ||
|
|
0b342f2c65 | ||
|
|
e8725b1a7c | ||
|
|
5ec1d3bb0c | ||
|
|
35efd1c0b9 | ||
|
|
8febd35998 | ||
|
|
94f668583e | ||
|
|
9e1bca3dc3 | ||
|
|
a9eb8cebd6 | ||
|
|
fa5bbbe7b9 | ||
|
|
8468317cf9 | ||
|
|
d315536956 | ||
|
|
165fcb51ff | ||
|
|
6f195aa0e6 | ||
|
|
b07d56c93c | ||
|
|
01fb6bfe61 | ||
|
|
06bd37f8d4 | ||
|
|
4f005b541d | ||
|
|
8426f43c5e | ||
|
|
963477a348 | ||
|
|
cdec3896a7 | ||
|
|
ba39f70e21 | ||
|
|
571aa67f39 | ||
|
|
8b01dfb8f0 | ||
|
|
e177a07c94 | ||
|
|
3ea6de756f | ||
|
|
be197a4ec5 | ||
|
|
fb92033555 | ||
|
|
4420ea8ffe | ||
|
|
e1c59e2dbe | ||
|
|
d269f8fa52 | ||
|
|
3ee9c0cddb | ||
|
|
fa97e9411a | ||
|
|
e2f91ffc8c | ||
|
|
9bc47b7fdc | ||
|
|
3d2f98bf7c | ||
|
|
1c875f2634 | ||
|
|
b752cf2aa9 | ||
|
|
a52166af4a | ||
|
|
83abb08534 | ||
|
|
fefad47ab4 | ||
|
|
125c896ef5 | ||
|
|
bc04f4ed8e | ||
|
|
25f2da417d | ||
|
|
79e16c8bf7 | ||
|
|
56a4899d2b | ||
|
|
6e86da8939 | ||
|
|
75dc2689c8 | ||
|
|
2fcfa04a3f | ||
|
|
78ec2744f1 | ||
|
|
6948fcfaec | ||
|
|
4ac43b38d5 | ||
|
|
590d3980be | ||
|
|
e569b90fcf | ||
|
|
49eb568990 | ||
|
|
698c8263ca | ||
|
|
b73f78dd74 | ||
|
|
b250f1d8c2 | ||
|
|
1764cfa03d | ||
|
|
7e33ae3e38 | ||
|
|
590a39ef14 | ||
|
|
154bd738ff | ||
|
|
831c29a774 | ||
|
|
8a5afc358c | ||
|
|
93f4189885 | ||
|
|
e1a53b122b | ||
|
|
2fafe2d752 | ||
|
|
1d82e562f4 | ||
|
|
a6a4315b7c | ||
|
|
f7aa67f522 | ||
|
|
ddcba7e4e8 | ||
|
|
f44ac86010 | ||
|
|
c02eb401a9 | ||
|
|
bc24ae2553 | ||
|
|
f423a53854 | ||
|
|
f9ca5a85ae | ||
|
|
a815cb8131 | ||
|
|
e488974ca8 | ||
|
|
55b5459854 | ||
|
|
76e02ac8e0 | ||
|
|
af19648fe9 | ||
|
|
28c8151b2c | ||
|
|
fa831a74ce | ||
|
|
3c2b7dade5 | ||
|
|
78b9c97de1 | ||
|
|
ae1aa591a2 | ||
|
|
7cae40b00b | ||
|
|
59b158ca0a | ||
|
|
915a9a4342 | ||
|
|
ffa59e66e1 | ||
|
|
1e8d2e8410 | ||
|
|
686511e21b | ||
|
|
8cb1901227 | ||
|
|
388eec726e | ||
|
|
6f696b4e94 | ||
|
|
5c07732b42 | ||
|
|
7949a544a4 | ||
|
|
3f0f6579ef | ||
|
|
efac33fba6 | ||
|
|
22a411fed9 | ||
|
|
83c3116ed9 | ||
|
|
07974c1359 | ||
|
|
d08528586c | ||
|
|
bb745d6a4e | ||
|
|
0f0edf1007 | ||
|
|
56f86a11b4 | ||
|
|
c9fe259a3a | ||
|
|
d163a050d8 | ||
|
|
9853d32b04 | ||
|
|
d0c43df71c | ||
|
|
219ce80e5b | ||
|
|
7985b310c5 | ||
|
|
501785d471 | ||
|
|
3ca32e9ed7 | ||
|
|
b6e5d329a5 | ||
|
|
c0821253ca | ||
|
|
bb8b1f2773 | ||
|
|
33b0dffba7 | ||
|
|
04cc6facaa | ||
|
|
e414204ac9 | ||
|
|
2975baafb9 | ||
|
|
5819d69d3e | ||
|
|
d0ab259047 | ||
|
|
058b07993c | ||
|
|
92fbf004c6 | ||
|
|
1d89bc4312 | ||
|
|
6626565a75 |
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
|
||||||
# Install the cosign tool except on PR
|
# Install the cosign tool except on PR
|
||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
# multi-platform images and export cache
|
# multi-platform images and export cache
|
||||||
# https://github.com/docker/setup-buildx-action
|
# https://github.com/docker/setup-buildx-action
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
|||||||
100
.semgrep.yml
Normal file
100
.semgrep.yml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# AutoGlue Semgrep configuration
|
||||||
|
# Use with: opengrep scan --config .semgrep.yml .
|
||||||
|
|
||||||
|
rules:
|
||||||
|
|
||||||
|
#
|
||||||
|
# 1. Suppress known benign “direct write to ResponseWriter” warnings
|
||||||
|
#
|
||||||
|
- id: autoglue.ignore.direct-write-static
|
||||||
|
message: Ignore direct writes for static or binary responses
|
||||||
|
languages: [go]
|
||||||
|
severity: INFO
|
||||||
|
metadata:
|
||||||
|
category: suppression
|
||||||
|
project: autoglue
|
||||||
|
patterns:
|
||||||
|
- pattern: |
|
||||||
|
_, _ = $W.Write($DATA)
|
||||||
|
pattern-inside: |
|
||||||
|
func $F($X...) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- internal/api/utils.go
|
||||||
|
- internal/handlers/ssh_keys.go
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 2. Enforce Allowed Origins checking in writePostMessageHTML
|
||||||
|
#
|
||||||
|
- id: autoglue.auth.require-origin-validation
|
||||||
|
message: >
|
||||||
|
writePostMessageHTML must validate `origin` against known allowed origins
|
||||||
|
to prevent token exfiltration via crafted state/redirect parameters.
|
||||||
|
languages: [go]
|
||||||
|
severity: ERROR
|
||||||
|
metadata:
|
||||||
|
category: security
|
||||||
|
project: autoglue
|
||||||
|
|
||||||
|
# Look for the JS snippet inside the Go string literal using a regex.
|
||||||
|
# This is NOT Go code, so we must use pattern-regex, not pattern.
|
||||||
|
pattern-regex: |
|
||||||
|
window\.opener\.postMessage\(\{ type: 'autoglue:auth', payload: data \}, .*?\);
|
||||||
|
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- internal/handlers/auth.go
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 3. Require httpOnly+Secure cookies for JWT cookies
|
||||||
|
#
|
||||||
|
- id: autoglue.cookies.ensure-secure-jwt
|
||||||
|
message: >
|
||||||
|
JWT cookies must always have a Secure field (true in prod, false only for localhost dev).
|
||||||
|
languages: [go]
|
||||||
|
severity: WARNING
|
||||||
|
metadata:
|
||||||
|
category: security
|
||||||
|
project: autoglue
|
||||||
|
|
||||||
|
patterns:
|
||||||
|
# 1) Find any SetCookie for ag_jwt
|
||||||
|
- pattern: |
|
||||||
|
http.SetCookie($W, &http.Cookie{
|
||||||
|
Name: "ag_jwt",
|
||||||
|
...
|
||||||
|
})
|
||||||
|
# 2) BUT ignore cases where the Secure field is present
|
||||||
|
- pattern-not: |
|
||||||
|
http.SetCookie($W, &http.Cookie{
|
||||||
|
Name: "ag_jwt",
|
||||||
|
Secure: $SECURE,
|
||||||
|
...
|
||||||
|
})
|
||||||
|
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- internal/handlers/auth.go
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 4. Ban path.Clean for user-controlled paths
|
||||||
|
#
|
||||||
|
- id: autoglue.filesystem.no-path-clean
|
||||||
|
message: Use securejoin instead of path.Clean() for file paths.
|
||||||
|
languages: [go]
|
||||||
|
severity: WARNING
|
||||||
|
metadata:
|
||||||
|
category: security
|
||||||
|
project: autoglue
|
||||||
|
|
||||||
|
pattern: |
|
||||||
|
path.Clean($X)
|
||||||
|
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- internal/web/static.go
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#################################
|
#################################
|
||||||
# Builder: Go + Node in one
|
# Builder: Go + Node in one
|
||||||
#################################
|
#################################
|
||||||
FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS builder
|
FROM golang:1.25.5-alpine@sha256:ac09a5f469f307e5da71e766b0bd59c9c49ea460a528cc3e6686513d64a6f1fb AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
bash git ca-certificates tzdata \
|
bash git ca-certificates tzdata \
|
||||||
@@ -24,7 +24,7 @@ RUN make build
|
|||||||
#################################
|
#################################
|
||||||
# Runtime
|
# Runtime
|
||||||
#################################
|
#################################
|
||||||
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
|
FROM alpine:3.23@sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata postgresql17-client \
|
RUN apk add --no-cache ca-certificates tzdata postgresql17-client \
|
||||||
&& addgroup -S app && adduser -S app -G app
|
&& addgroup -S app && adduser -S app -G app
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -30,6 +30,7 @@ UI_SSG_ROUTES ?= /,/login,/docs,/pricing
|
|||||||
|
|
||||||
# Go versioning (go.mod uses major.minor; you’re on 1.25.4)
|
# Go versioning (go.mod uses major.minor; you’re on 1.25.4)
|
||||||
GO_VERSION ?= 1.25.4
|
GO_VERSION ?= 1.25.4
|
||||||
|
SWAG_FLAGS ?= --v3.1 --outputTypes json,yaml,go
|
||||||
|
|
||||||
# SDK / package settings (TypeScript)
|
# SDK / package settings (TypeScript)
|
||||||
SDK_TS_OUTDIR ?= sdk/ts
|
SDK_TS_OUTDIR ?= sdk/ts
|
||||||
@@ -98,8 +99,8 @@ SDK_PKG_CLEAN := $(call trim,$(SDK_PKG))
|
|||||||
validate-spec check-tags doctor diff-swagger
|
validate-spec check-tags doctor diff-swagger
|
||||||
|
|
||||||
# --- inputs/outputs for swagger (incremental) ---
|
# --- inputs/outputs for swagger (incremental) ---
|
||||||
DOCS_JSON := docs/swagger.json
|
DOCS_JSON := docs/openapi.json
|
||||||
DOCS_YAML := docs/swagger.yaml
|
DOCS_YAML := docs/openapi.yaml
|
||||||
# Prefer git for speed; fall back to find. Exclude UI dir.
|
# Prefer git for speed; fall back to find. Exclude UI dir.
|
||||||
#GO_SRCS := $(shell (git ls-files '*.go' ':!$(UI_DIR)/**' 2>/dev/null || find . -name '*.go' -not -path './$(UI_DIR)/*' -type f))
|
#GO_SRCS := $(shell (git ls-files '*.go' ':!$(UI_DIR)/**' 2>/dev/null || find . -name '*.go' -not -path './$(UI_DIR)/*' -type f))
|
||||||
GO_SRCS := $(shell ( \
|
GO_SRCS := $(shell ( \
|
||||||
@@ -111,11 +112,14 @@ GO_SRCS := $(shell ( \
|
|||||||
$(DOCS_JSON) $(DOCS_YAML): $(GO_SRCS)
|
$(DOCS_JSON) $(DOCS_YAML): $(GO_SRCS)
|
||||||
@echo ">> Generating Swagger docs..."
|
@echo ">> Generating Swagger docs..."
|
||||||
@if ! command -v swag >/dev/null 2>&1; then \
|
@if ! command -v swag >/dev/null 2>&1; then \
|
||||||
echo "Installing swag/v2 CLI @v2.0.0-rc4..."; \
|
echo "Installing swag/v2 CLI @latest..."; \
|
||||||
$(GOINSTALL) github.com/swaggo/swag/v2/cmd/swag@v2.0.0-rc4; \
|
$(GOINSTALL) github.com/swaggo/swag/v2/cmd/swag@latest; \
|
||||||
fi
|
fi
|
||||||
@rm -rf docs/swagger.* docs/docs.go
|
@rm -rf docs/openapi.* docs/docs.go
|
||||||
@swag init -g $(MAIN) -o docs
|
@swag fmt --exclude main.go -d .
|
||||||
|
@swag init $(SWAG_FLAGS) -g $(MAIN) -o docs
|
||||||
|
@mv docs/swagger.json $(DOCS_JSON)
|
||||||
|
@mv docs/swagger.yaml $(DOCS_YAML)
|
||||||
|
|
||||||
# --- spec validation + tag guard ---
|
# --- spec validation + tag guard ---
|
||||||
validate-spec: $(DOCS_JSON) ## Validate docs/swagger.json and pin the core OpenAPI Generator version
|
validate-spec: $(DOCS_JSON) ## Validate docs/swagger.json and pin the core OpenAPI Generator version
|
||||||
@@ -200,6 +204,7 @@ swagger: $(DOCS_JSON) ## Generate Swagger docs if stale
|
|||||||
# --- build ---
|
# --- build ---
|
||||||
build: prepare ui swagger sdk-all ## Build everything: Go hygiene, UI, Swagger, SDKs, then Go binary
|
build: prepare ui swagger sdk-all ## Build everything: Go hygiene, UI, Swagger, SDKs, then Go binary
|
||||||
@echo ">> Building Go binary: $(BIN)"
|
@echo ">> Building Go binary: $(BIN)"
|
||||||
|
@$(GOCMD) get github.com/swaggo/swag/v2@v2.0.0-rc4
|
||||||
@$(GOCMD) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN) $(MAIN)
|
@$(GOCMD) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN) $(MAIN)
|
||||||
|
|
||||||
# Handy: print resolved version metadata
|
# Handy: print resolved version metadata
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ Create your org (http://localhost:8080/me) - you should be redirected here after
|
|||||||
|
|
||||||
Once you have an org - create a set of api keys for your org:
|
Once you have an org - create a set of api keys for your org:
|
||||||
They will be in the format of:
|
They will be in the format of:
|
||||||
|
Example values only; these are not real secrets.
|
||||||
```text
|
```text
|
||||||
Org Key: org_lnJwmyyWH7JC-JgZo5v3Kw
|
Org Key: org_lnJwmyyWH7JC-JgZo5v3Kw
|
||||||
Org Secret: fqd9yebGMfK6h5HSgWn4sXrwr9xlFbvbIYtNylRElMQ
|
Org Secret: fqd9yebGMfK6h5HSgWn4sXrwr9xlFbvbIYtNylRElMQ
|
||||||
|
|||||||
20
atlas.hcl
Normal file
20
atlas.hcl
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
data "external_schema" "gorm" {
|
||||||
|
program = [
|
||||||
|
"go",
|
||||||
|
"run",
|
||||||
|
"-mod=mod",
|
||||||
|
"ariga.io/atlas-provider-gorm",
|
||||||
|
"load",
|
||||||
|
"--path", "./internal/models",
|
||||||
|
"--dialect", "postgres",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
env "gorm" {
|
||||||
|
src = data.external_schema.gorm.url
|
||||||
|
dev = "postgres://autoglue:autoglue@localhost:5432/autoglue_dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
env "gorm-src" {
|
||||||
|
src = data.external_schema.gorm.url
|
||||||
|
}
|
||||||
60
cmd/serve.go
60
cmd/serve.go
@@ -115,6 +115,64 @@ var serveCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_, err = jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"prepare_cluster",
|
||||||
|
bg.ClusterPrepareArgs{IntervalS: 120},
|
||||||
|
archer.WithMaxRetries(3),
|
||||||
|
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue prepare_cluster: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"cluster_setup",
|
||||||
|
bg.ClusterSetupArgs{
|
||||||
|
IntervalS: 120,
|
||||||
|
},
|
||||||
|
archer.WithMaxRetries(3),
|
||||||
|
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue cluster setup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"cluster_bootstrap",
|
||||||
|
bg.ClusterBootstrapArgs{
|
||||||
|
IntervalS: 120,
|
||||||
|
},
|
||||||
|
archer.WithMaxRetries(3),
|
||||||
|
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue cluster bootstrap: %v", err)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
_, err = jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"org_key_sweeper",
|
||||||
|
bg.OrgKeySweeperArgs{
|
||||||
|
IntervalS: 3600,
|
||||||
|
RetentionDays: 10,
|
||||||
|
},
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
archer.WithScheduleTime(time.Now()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue org_key_sweeper: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
||||||
@@ -134,7 +192,7 @@ var serveCmd = &cobra.Command{
|
|||||||
dbURL = cfg.DbURL
|
dbURL = cfg.DbURL
|
||||||
}
|
}
|
||||||
|
|
||||||
studio, err := api.PgwebHandler(
|
studio, err := api.MountDbStudio(
|
||||||
dbURL,
|
dbURL,
|
||||||
"db-studio",
|
"db-studio",
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -1,18 +1,4 @@
|
|||||||
services:
|
services:
|
||||||
autoglue:
|
|
||||||
# image: ghcr.io/glueops/autoglue:latest
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
expose:
|
|
||||||
- 8080
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
AUTOGLUE_DATABASE_DSN: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
|
|
||||||
AUTOGLUE_BIND_ADDRESS: 0.0.0.0
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
build:
|
build:
|
||||||
context: postgres
|
context: postgres
|
||||||
@@ -28,19 +14,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
pgweb:
|
|
||||||
image: sosedoff/pgweb@sha256:8f1ed22e10c9da0912169b98b62ddc54930dc39a5ae07b0f1354d2a93d44c6ed
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "8081:8081"
|
|
||||||
links:
|
|
||||||
- postgres:postgres
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
PGWEB_DATABASE_URL: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
|
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit@sha256:e22dce5b36f93c77082e204a3942fb6b283b7896e057458400a4c88344c3df68
|
image: axllent/mailpit@sha256:e22dce5b36f93c77082e204a3942fb6b283b7896e057458400a4c88344c3df68
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
19
docs/docs.go
19
docs/docs.go
File diff suppressed because one or more lines are too long
@@ -2,8 +2,8 @@ package docs
|
|||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
//go:embed swagger.json
|
//go:embed openapi.json
|
||||||
var SwaggerJSON []byte
|
var SwaggerJSON []byte
|
||||||
|
|
||||||
//go:embed swagger.yaml
|
//go:embed openapi.yaml
|
||||||
var SwaggerYAML []byte
|
var SwaggerYAML []byte
|
||||||
|
|||||||
13
docs/openapi.json
Normal file
13
docs/openapi.json
Normal file
File diff suppressed because one or more lines are too long
7234
docs/openapi.yaml
Normal file
7234
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
4734
docs/swagger.yaml
4734
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
80
go.mod
80
go.mod
@@ -4,29 +4,30 @@ go 1.25.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexedwards/argon2id v1.0.0
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.6
|
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.18
|
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0
|
github.com/aws/smithy-go v1.24.0
|
||||||
github.com/dyaksa/archer v1.1.3
|
github.com/coreos/go-oidc/v3 v3.17.0
|
||||||
|
github.com/dyaksa/archer v1.1.5
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.33.0
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/httprate v0.15.0
|
github.com/go-chi/httprate v0.15.0
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/sosedoff/pgweb v0.16.2
|
github.com/sosedoff/pgweb v0.17.0
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
|
||||||
github.com/swaggo/swag/v2 v2.0.0-rc4
|
github.com/swaggo/swag/v2 v2.0.0-rc4
|
||||||
golang.org/x/crypto v0.44.0
|
golang.org/x/crypto v0.46.0
|
||||||
golang.org/x/oauth2 v0.33.0
|
golang.org/x/oauth2 v0.34.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/datatypes v1.2.7
|
gorm.io/datatypes v1.2.7
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
@@ -38,28 +39,29 @@ require (
|
|||||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // indirect
|
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // 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/internal/v4a v1.4.16 // 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/accept-encoding v1.13.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // 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/presigned-url v1.13.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||||
@@ -89,18 +91,21 @@ require (
|
|||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.48.0 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
@@ -109,22 +114,21 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/sv-tools/openapi v0.2.1 // indirect
|
github.com/sv-tools/openapi v0.2.1 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
|
||||||
github.com/swaggo/swag v1.8.1 // indirect
|
|
||||||
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 // indirect
|
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/net v0.46.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gorm.io/driver/mysql v1.5.6 // indirect
|
gorm.io/driver/mysql v1.5.7 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
202
go.sum
202
go.sum
@@ -8,100 +8,81 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
|||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
|
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
|
||||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
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 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
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.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
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.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||||
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/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI=
|
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg=
|
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.19 h1:qdUtOw4JhZr2YcKO3g0ho/IcFXfXrrb8xlX05Y6EvSw=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.19/go.mod h1:tMJ8bur01t8eEm0atLadkIIFA154OJ4JCKZeQ+o+R7k=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.23 h1:IQILcxVgMO2BVLaJ2aAv21dKWvE1MduNrbvuK43XL2Q=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.23/go.mod h1:JRodHszhVdh5TPUknxDzJzrMiznG+M+FfR3WSWKgCI8=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||||
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 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/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.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||||
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.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||||
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/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||||
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.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||||
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/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||||
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.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||||
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.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||||
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.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE=
|
||||||
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/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA=
|
||||||
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.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
|
||||||
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.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||||
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.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||||
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.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||||
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/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||||
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/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||||
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.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||||
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.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 h1:/p6MxkbQoCzaGQT3WO0JwG0FlQyG9RD8VmdmoKc5xqU=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||||
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/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||||
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/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
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 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
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 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
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.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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 h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
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.5 h1:e9ZrR8PnMYEax19Fd+QbvQqDL5cbi78luMEtaSAIHxU=
|
||||||
github.com/dyaksa/archer v1.1.3/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4=
|
github.com/dyaksa/archer v1.1.5/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4=
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.33.0 h1:ka8vmRpm4IDsES7NPXQ/NThAp1fc/f+crcXYjCW7wK0=
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.33.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
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-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 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
@@ -133,8 +114,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
@@ -208,8 +189,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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-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.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.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
@@ -222,18 +203,17 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
|||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
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/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
|
||||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
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_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.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
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/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 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
@@ -242,8 +222,8 @@ 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/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 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
@@ -252,16 +232,16 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
|
|||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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.17.0 h1:2WSPajNyqStS5oulvfdKIBaWQTy/qNBREBp51h4yiLU=
|
||||||
github.com/sosedoff/pgweb v0.16.2/go.mod h1:ER7fsBddI3h7MQKO5RsUPi7Q/PWZYSKcI61kTp369Rw=
|
github.com/sosedoff/pgweb v0.17.0/go.mod h1:fY82HStJ/n/JCvzHsJmVT6BDYiWxSQG6CvqH+biuUbM=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
@@ -285,12 +265,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA=
|
github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA=
|
||||||
github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg=
|
github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg=
|
||||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
|
||||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
|
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
|
||||||
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 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
|
||||||
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
|
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
|
||||||
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 h1:yL0l/u242MzDP6D0B5vGC+wxm5WRY+alQZy+dJk3bFI=
|
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 h1:yL0l/u242MzDP6D0B5vGC+wxm5WRY+alQZy+dJk3bFI=
|
||||||
@@ -299,11 +273,15 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
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 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
@@ -314,27 +292,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -350,29 +328,29 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
@@ -390,8 +368,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
|||||||
@@ -22,5 +22,16 @@ func mountAdminRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authUser func(ht
|
|||||||
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
||||||
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
||||||
})
|
})
|
||||||
|
admin.Route("/actions", func(action chi.Router) {
|
||||||
|
action.Use(authUser)
|
||||||
|
action.Use(httpmiddleware.RequirePlatformAdmin())
|
||||||
|
|
||||||
|
action.Get("/", handlers.ListActions(db))
|
||||||
|
action.Post("/", handlers.CreateAction(db))
|
||||||
|
|
||||||
|
action.Get("/{actionID}", handlers.GetAction(db))
|
||||||
|
action.Patch("/{actionID}", handlers.UpdateAction(db))
|
||||||
|
action.Delete("/{actionID}", handlers.DeleteAction(db))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ func mountAPIRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs) {
|
|||||||
mountAnnotationRoutes(v1, db, authOrg)
|
mountAnnotationRoutes(v1, db, authOrg)
|
||||||
mountNodePoolRoutes(v1, db, authOrg)
|
mountNodePoolRoutes(v1, db, authOrg)
|
||||||
mountDNSRoutes(v1, db, authOrg)
|
mountDNSRoutes(v1, db, authOrg)
|
||||||
|
mountLoadBalancerRoutes(v1, db, authOrg)
|
||||||
|
mountClusterRoutes(v1, db, jobs, authOrg)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
46
internal/api/mount_cluster_routes.go
Normal file
46
internal/api/mount_cluster_routes.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountClusterRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/clusters", func(c chi.Router) {
|
||||||
|
c.Use(authOrg)
|
||||||
|
c.Get("/", handlers.ListClusters(db))
|
||||||
|
c.Post("/", handlers.CreateCluster(db))
|
||||||
|
|
||||||
|
c.Get("/{clusterID}", handlers.GetCluster(db))
|
||||||
|
c.Patch("/{clusterID}", handlers.UpdateCluster(db))
|
||||||
|
c.Delete("/{clusterID}", handlers.DeleteCluster(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/captain-domain", handlers.AttachCaptainDomain(db))
|
||||||
|
c.Delete("/{clusterID}/captain-domain", handlers.DetachCaptainDomain(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/control-plane-record-set", handlers.AttachControlPlaneRecordSet(db))
|
||||||
|
c.Delete("/{clusterID}/control-plane-record-set", handlers.DetachControlPlaneRecordSet(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/apps-load-balancer", handlers.AttachAppsLoadBalancer(db))
|
||||||
|
c.Delete("/{clusterID}/apps-load-balancer", handlers.DetachAppsLoadBalancer(db))
|
||||||
|
c.Post("/{clusterID}/glueops-load-balancer", handlers.AttachGlueOpsLoadBalancer(db))
|
||||||
|
c.Delete("/{clusterID}/glueops-load-balancer", handlers.DetachGlueOpsLoadBalancer(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/bastion", handlers.AttachBastionServer(db))
|
||||||
|
c.Delete("/{clusterID}/bastion", handlers.DetachBastionServer(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/kubeconfig", handlers.SetClusterKubeconfig(db))
|
||||||
|
c.Delete("/{clusterID}/kubeconfig", handlers.ClearClusterKubeconfig(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/node-pools", handlers.AttachNodePool(db))
|
||||||
|
c.Delete("/{clusterID}/node-pools/{nodePoolID}", handlers.DetachNodePool(db))
|
||||||
|
|
||||||
|
c.Get("/{clusterID}/runs", handlers.ListClusterRuns(db))
|
||||||
|
c.Get("/{clusterID}/runs/{runID}", handlers.GetClusterRun(db))
|
||||||
|
c.Post("/{clusterID}/actions/{actionID}/runs", handlers.RunClusterAction(db, jobs))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
pgcmd "github.com/sosedoff/pgweb/pkg/command"
|
pgcmd "github.com/sosedoff/pgweb/pkg/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PgwebHandler(dbURL, prefix string, readonly bool) (http.Handler, error) {
|
func MountDbStudio(dbURL, prefix string, readonly bool) (http.Handler, error) {
|
||||||
// Normalize prefix for pgweb:
|
// Normalize prefix for pgweb:
|
||||||
// - no leading slash
|
// - no leading slash
|
||||||
// - always trailing slash if not empty
|
// - always trailing slash if not empty
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func mountDNSRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.H
|
|||||||
|
|
||||||
d.Get("/domains/{domain_id}/records", handlers.ListRecordSets(db))
|
d.Get("/domains/{domain_id}/records", handlers.ListRecordSets(db))
|
||||||
d.Post("/domains/{domain_id}/records", handlers.CreateRecordSet(db))
|
d.Post("/domains/{domain_id}/records", handlers.CreateRecordSet(db))
|
||||||
|
d.Get("/records/{id}", handlers.GetRecordSet(db))
|
||||||
d.Patch("/records/{id}", handlers.UpdateRecordSet(db))
|
d.Patch("/records/{id}", handlers.UpdateRecordSet(db))
|
||||||
d.Delete("/records/{id}", handlers.DeleteRecordSet(db))
|
d.Delete("/records/{id}", handlers.DeleteRecordSet(db))
|
||||||
})
|
})
|
||||||
|
|||||||
20
internal/api/mount_load_balancer_routes.go
Normal file
20
internal/api/mount_load_balancer_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 mountLoadBalancerRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||||
|
r.Route("/load-balancers", func(l chi.Router) {
|
||||||
|
l.Use(authOrg)
|
||||||
|
l.Get("/", handlers.ListLoadBalancers(db))
|
||||||
|
l.Post("/", handlers.CreateLoadBalancer(db))
|
||||||
|
l.Get("/{id}", handlers.GetLoadBalancer(db))
|
||||||
|
l.Patch("/{id}", handlers.UpdateLoadBalancer(db))
|
||||||
|
l.Delete("/{id}", handlers.DeleteLoadBalancer(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,15 +1,87 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/glueops/autoglue/docs"
|
"github.com/glueops/autoglue/docs"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func mountSwaggerRoutes(r chi.Router) {
|
func mountSwaggerRoutes(r chi.Router) {
|
||||||
r.Get("/swagger/*", httpSwagger.Handler(
|
r.Get("/swagger", RapidDocHandler("/swagger/swagger.yaml"))
|
||||||
httpSwagger.URL("swagger.json"),
|
r.Get("/swagger/index.html", RapidDocHandler("/swagger/swagger.yaml"))
|
||||||
))
|
r.Get("/swagger/openapi.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||||
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
r.Get("/swagger/openapi.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
||||||
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
}
|
||||||
|
|
||||||
|
var rapidDocTmpl = template.Must(template.New("redoc").Parse(`<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>AutoGlue API Docs</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
.redoc-container { height: 100vh; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<rapi-doc
|
||||||
|
id="autoglue-docs"
|
||||||
|
spec-url="{{.SpecURL}}"
|
||||||
|
render-style="read"
|
||||||
|
theme="dark"
|
||||||
|
show-header="false"
|
||||||
|
persist-auth="true"
|
||||||
|
allow-advanced-search="true"
|
||||||
|
schema-description-expanded="true"
|
||||||
|
allow-schema-description-expand-toggle="false"
|
||||||
|
allow-spec-file-download="true"
|
||||||
|
allow-spec-file-load="false"
|
||||||
|
allow-spec-url-load="false"
|
||||||
|
allow-try="true"
|
||||||
|
schema-style="tree"
|
||||||
|
fetch-credentials="include"
|
||||||
|
default-api-server="{{.DefaultServer}}"
|
||||||
|
api-key-name="X-ORG-ID"
|
||||||
|
api-key-location="header"
|
||||||
|
api-key-value=""
|
||||||
|
/>
|
||||||
|
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const rd = document.getElementById('autoglue-docs');
|
||||||
|
if (!rd) return;
|
||||||
|
|
||||||
|
const storedOrg = localStorage.getItem('autoglue.org');
|
||||||
|
if (storedOrg) {
|
||||||
|
rd.setAttribute('api-key-value', storedOrg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
|
|
||||||
|
func RapidDocHandler(specURL string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
host := r.Host
|
||||||
|
defaultServer := fmt.Sprintf("%s://%s/api/v1", scheme, host)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := rapidDocTmpl.Execute(w, map[string]string{
|
||||||
|
"SpecURL": specURL,
|
||||||
|
"DefaultServer": defaultServer,
|
||||||
|
}); err != nil {
|
||||||
|
http.Error(w, "failed to render docs", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
|||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
// Vite dev & inline preamble/eval:
|
// Vite dev & inline preamble/eval:
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:5173",
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:5173 https://unpkg.com",
|
||||||
// allow dev style + Google Fonts
|
// allow dev style + Google Fonts
|
||||||
"style-src 'self' 'unsafe-inline' http://localhost:5173 https://fonts.googleapis.com",
|
"style-src 'self' 'unsafe-inline' http://localhost:5173 https://fonts.googleapis.com",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
// Google font files
|
// Google font files
|
||||||
"font-src 'self' data: https://fonts.gstatic.com",
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
// HMR connections
|
// HMR connections
|
||||||
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080 https://api.github.com",
|
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080 https://api.github.com https://unpkg.com",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
}, "; "))
|
}, "; "))
|
||||||
} else {
|
} else {
|
||||||
@@ -49,11 +49,11 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
|||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
"script-src 'self' 'unsafe-inline'",
|
"script-src 'self' 'unsafe-inline' https://unpkg.com",
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
"font-src 'self' data: https://fonts.gstatic.com",
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
"connect-src 'self' ws://localhost:8080 https://api.github.com",
|
"connect-src 'self' ws://localhost:8080 https://api.github.com https://unpkg.com",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
}, "; "))
|
}, "; "))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
|||||||
r.Use(SecurityHeaders)
|
r.Use(SecurityHeaders)
|
||||||
r.Use(requestBodyLimit(10 << 20))
|
r.Use(requestBodyLimit(10 << 20))
|
||||||
r.Use(httprate.LimitByIP(100, 1*time.Minute))
|
r.Use(httprate.LimitByIP(100, 1*time.Minute))
|
||||||
|
r.Use(middleware.StripSlashes)
|
||||||
|
|
||||||
allowed := getAllowedOrigins()
|
allowed := getAllowedOrigins()
|
||||||
r.Use(cors.Handler(cors.Options{
|
r.Use(cors.Handler(cors.Options{
|
||||||
@@ -103,18 +104,19 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/api/", r)
|
mux.Handle("/api/", r)
|
||||||
mux.Handle("/api", r)
|
mux.Handle("/api", r)
|
||||||
|
mux.Handle("/swagger", r)
|
||||||
mux.Handle("/swagger/", r)
|
mux.Handle("/swagger/", r)
|
||||||
mux.Handle("/db-studio/", r)
|
mux.Handle("/db-studio/", r)
|
||||||
mux.Handle("/debug/pprof/", r)
|
mux.Handle("/debug/pprof/", r)
|
||||||
mux.Handle("/", proxy)
|
mux.Handle("/", proxy)
|
||||||
return mux
|
return mux
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Running in production mode")
|
|
||||||
if h, err := web.SPAHandler(); err == nil {
|
|
||||||
r.NotFound(h.ServeHTTP)
|
|
||||||
} else {
|
} else {
|
||||||
log.Error().Err(err).Msg("spa handler init failed")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func serveSwaggerFromEmbed(data []byte, contentType string) http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, _ *http.Request) {
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set("Content-Type", contentType)
|
w.Header().Set("Content-Type", contentType)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
// nosemgrep: go.lang.security.audit.xss.no-direct-write-to-responsewriter
|
||||||
_, _ = w.Write(data)
|
_, _ = w.Write(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,14 @@ func NewRuntime() *Runtime {
|
|||||||
&models.Label{},
|
&models.Label{},
|
||||||
&models.Annotation{},
|
&models.Annotation{},
|
||||||
&models.NodePool{},
|
&models.NodePool{},
|
||||||
&models.Cluster{},
|
|
||||||
&models.Credential{},
|
&models.Credential{},
|
||||||
&models.Domain{},
|
&models.Domain{},
|
||||||
&models.RecordSet{},
|
&models.RecordSet{},
|
||||||
|
&models.LoadBalancer{},
|
||||||
|
&models.Cluster{},
|
||||||
|
&models.Action{},
|
||||||
|
&models.Cluster{},
|
||||||
|
&models.ClusterRun{},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -140,10 +140,7 @@ func BastionBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
|||||||
_ = setServerStatus(db, s.ID, "failed")
|
_ = setServerStatus(db, s.ID, "failed")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ok++
|
ok++
|
||||||
// logHostInfo(jobID, s, "done", "host completed",
|
|
||||||
// "elapsed_ms", time.Since(hostStart).Milliseconds())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res := BastionBootstrapResult{
|
res := BastionBootstrapResult{
|
||||||
|
|||||||
@@ -107,6 +107,38 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
|||||||
archer.WithInstances(1),
|
archer.WithInstances(1),
|
||||||
archer.WithTimeout(2*time.Minute),
|
archer.WithTimeout(2*time.Minute),
|
||||||
)
|
)
|
||||||
|
/*
|
||||||
|
c.Register(
|
||||||
|
"prepare_cluster",
|
||||||
|
ClusterPrepareWorker(gdb, jobs),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(2*time.Minute),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Register(
|
||||||
|
"cluster_setup",
|
||||||
|
ClusterSetupWorker(gdb, jobs),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(2*time.Minute),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Register(
|
||||||
|
"cluster_bootstrap",
|
||||||
|
ClusterBootstrapWorker(gdb, jobs),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(60*time.Minute),
|
||||||
|
)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
c.Register(
|
||||||
|
"org_key_sweeper",
|
||||||
|
OrgKeySweeperWorker(gdb, jobs),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(5*time.Minute),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Register("cluster_action", ClusterActionWorker(gdb))
|
||||||
return jobs, nil
|
return jobs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
195
internal/bg/cluster_action.go
Normal file
195
internal/bg/cluster_action.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/mapper"
|
||||||
|
"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 ClusterActionArgs struct {
|
||||||
|
OrgID uuid.UUID `json:"org_id"`
|
||||||
|
ClusterID uuid.UUID `json:"cluster_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
MakeTarget string `json:"make_target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterActionResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ClusterID string `json:"cluster_id"`
|
||||||
|
ElapsedMs int `json:"elapsed_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClusterActionWorker(db *gorm.DB) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
start := time.Now()
|
||||||
|
var args ClusterActionArgs
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
|
||||||
|
runID, _ := uuid.Parse(j.ID)
|
||||||
|
|
||||||
|
updateRun := func(status string, errMsg string) {
|
||||||
|
updates := map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"error": errMsg,
|
||||||
|
}
|
||||||
|
if status == "succeeded" || status == "failed" {
|
||||||
|
updates["finished_at"] = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
db.Model(&models.ClusterRun{}).Where("id = ?", runID).Updates(updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRun("running", "")
|
||||||
|
|
||||||
|
logger := log.With().
|
||||||
|
Str("job", j.ID).
|
||||||
|
Str("cluster_id", args.ClusterID.String()).
|
||||||
|
Str("action", args.Action).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
var c models.Cluster
|
||||||
|
if err := db.
|
||||||
|
Preload("BastionServer.SshKey").
|
||||||
|
Preload("CaptainDomain").
|
||||||
|
Preload("ControlPlaneRecordSet").
|
||||||
|
Preload("AppsLoadBalancer").
|
||||||
|
Preload("GlueOpsLoadBalancer").
|
||||||
|
Preload("NodePools").
|
||||||
|
Preload("NodePools.Labels").
|
||||||
|
Preload("NodePools.Annotations").
|
||||||
|
Preload("NodePools.Taints").
|
||||||
|
Preload("NodePools.Servers.SshKey").
|
||||||
|
Where("id = ? AND organization_id = ?", args.ClusterID, args.OrgID).
|
||||||
|
First(&c).Error; err != nil {
|
||||||
|
updateRun("failed", fmt.Errorf("load cluster: %w", err).Error())
|
||||||
|
return nil, fmt.Errorf("load cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Step 1: Prepare (mostly lifted from ClusterPrepareWorker)
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusBootstrapping, ""); err != nil {
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("mark bootstrapping: %w", err)
|
||||||
|
}
|
||||||
|
c.Status = clusterStatusBootstrapping
|
||||||
|
|
||||||
|
if err := validateClusterForPrepare(&c); err != nil {
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("validate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allServers := flattenClusterServers(&c)
|
||||||
|
keyPayloads, sshConfig, err := buildSSHAssetsForCluster(db, &c, allServers)
|
||||||
|
if err != nil {
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("build ssh assets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dtoCluster := mapper.ClusterToDTO(c)
|
||||||
|
|
||||||
|
if c.EncryptedKubeconfig != "" && c.KubeIV != "" && c.KubeTag != "" {
|
||||||
|
kubeconfig, err := utils.DecryptForOrg(
|
||||||
|
c.OrganizationID,
|
||||||
|
c.EncryptedKubeconfig,
|
||||||
|
c.KubeIV,
|
||||||
|
c.KubeTag,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
return nil, fmt.Errorf("decrypt kubeconfig: %w", err)
|
||||||
|
}
|
||||||
|
dtoCluster.Kubeconfig = &kubeconfig
|
||||||
|
}
|
||||||
|
|
||||||
|
orgKey, orgSecret, err := findOrCreateClusterAutomationKey(db, c.OrganizationID, c.ID, 24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("org key: %w", err)
|
||||||
|
}
|
||||||
|
dtoCluster.OrgKey = &orgKey
|
||||||
|
dtoCluster.OrgSecret = &orgSecret
|
||||||
|
|
||||||
|
payloadJSON, err := json.MarshalIndent(dtoCluster, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("marshal payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
runCtx, cancel := context.WithTimeout(ctx, 8*time.Minute)
|
||||||
|
err := pushAssetsToBastion(runCtx, db, &c, sshConfig, keyPayloads, payloadJSON)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("push assets: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusPending, ""); err != nil {
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("mark pending: %w", err)
|
||||||
|
}
|
||||||
|
c.Status = clusterStatusPending
|
||||||
|
|
||||||
|
// ---- Step 2: Setup (ping-servers)
|
||||||
|
{
|
||||||
|
runCtx, cancel := context.WithTimeout(ctx, 30*time.Minute)
|
||||||
|
out, err := runMakeOnBastion(runCtx, db, &c, "ping-servers")
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Str("output", out).Msg("ping-servers failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make ping-servers: %v", err))
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("ping-servers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusProvisioning, ""); err != nil {
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("mark provisioning: %w", err)
|
||||||
|
}
|
||||||
|
c.Status = clusterStatusProvisioning
|
||||||
|
|
||||||
|
// ---- Step 3: Bootstrap (parameterized target)
|
||||||
|
{
|
||||||
|
runCtx, cancel := context.WithTimeout(ctx, 60*time.Minute)
|
||||||
|
out, err := runMakeOnBastion(runCtx, db, &c, args.MakeTarget)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Str("output", out).Msg("bootstrap target failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make %s: %v", args.MakeTarget, err))
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("make %s: %w", args.MakeTarget, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusReady, ""); err != nil {
|
||||||
|
updateRun("failed", err.Error())
|
||||||
|
return nil, fmt.Errorf("mark ready: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRun("succeeded", "")
|
||||||
|
|
||||||
|
return ClusterActionResult{
|
||||||
|
Status: "ok",
|
||||||
|
Action: args.Action,
|
||||||
|
ClusterID: c.ID.String(),
|
||||||
|
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/bg/cluster_bootstrap.go
Normal file
121
internal/bg/cluster_bootstrap.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterBootstrapArgs struct {
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterBootstrapResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Processed int `json:"processed"`
|
||||||
|
Ready int `json:"ready"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
ElapsedMs int `json:"elapsed_ms"`
|
||||||
|
FailedIDs []uuid.UUID `json:"failed_cluster_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClusterBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := ClusterBootstrapArgs{IntervalS: 120}
|
||||||
|
jobID := j.ID
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 120
|
||||||
|
}
|
||||||
|
|
||||||
|
var clusters []models.Cluster
|
||||||
|
if err := db.
|
||||||
|
Preload("BastionServer.SshKey").
|
||||||
|
Where("status = ?", clusterStatusProvisioning).
|
||||||
|
Find(&clusters).Error; err != nil {
|
||||||
|
log.Error().Err(err).Msg("[cluster_bootstrap] query clusters failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, ready, failCount := 0, 0, 0
|
||||||
|
var failedIDs []uuid.UUID
|
||||||
|
|
||||||
|
perClusterTimeout := 60 * time.Minute
|
||||||
|
|
||||||
|
for i := range clusters {
|
||||||
|
c := &clusters[i]
|
||||||
|
proc++
|
||||||
|
|
||||||
|
if c.BastionServer.ID == uuid.Nil || c.BastionServer.Status != "ready" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := log.With().
|
||||||
|
Str("job", jobID).
|
||||||
|
Str("cluster_id", c.ID.String()).
|
||||||
|
Str("cluster_name", c.Name).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
logger.Info().Msg("[cluster_bootstrap] running make bootstrap")
|
||||||
|
|
||||||
|
runCtx, cancel := context.WithTimeout(ctx, perClusterTimeout)
|
||||||
|
out, err := runMakeOnBastion(runCtx, db, c, "setup")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
failCount++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
logger.Error().Err(err).Str("output", out).Msg("[cluster_bootstrap] make setup failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make setup: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// you can choose a different terminal status here if you like
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusReady, ""); err != nil {
|
||||||
|
failCount++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
logger.Error().Err(err).Msg("[cluster_bootstrap] failed to mark cluster ready")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ready++
|
||||||
|
logger.Info().Msg("[cluster_bootstrap] cluster marked ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := ClusterBootstrapResult{
|
||||||
|
Status: "ok",
|
||||||
|
Processed: proc,
|
||||||
|
Ready: ready,
|
||||||
|
Failed: failCount,
|
||||||
|
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||||
|
FailedIDs: failedIDs,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("processed", proc).
|
||||||
|
Int("ready", ready).
|
||||||
|
Int("failed", failCount).
|
||||||
|
Msg("[cluster_bootstrap] reconcile tick ok")
|
||||||
|
|
||||||
|
// self-reschedule
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"cluster_bootstrap",
|
||||||
|
args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
120
internal/bg/cluster_setup.go
Normal file
120
internal/bg/cluster_setup.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterSetupArgs struct {
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterSetupResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Processed int `json:"processed"`
|
||||||
|
Provisioning int `json:"provisioning"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
ElapsedMs int `json:"elapsed_ms"`
|
||||||
|
FailedCluster []uuid.UUID `json:"failed_cluster_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClusterSetupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := ClusterSetupArgs{IntervalS: 120}
|
||||||
|
jobID := j.ID
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 120
|
||||||
|
}
|
||||||
|
|
||||||
|
var clusters []models.Cluster
|
||||||
|
if err := db.
|
||||||
|
Preload("BastionServer.SshKey").
|
||||||
|
Where("status = ?", clusterStatusPending).
|
||||||
|
Find(&clusters).Error; err != nil {
|
||||||
|
log.Error().Err(err).Msg("[cluster_setup] query clusters failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, prov, failCount := 0, 0, 0
|
||||||
|
var failedIDs []uuid.UUID
|
||||||
|
|
||||||
|
perClusterTimeout := 30 * time.Minute
|
||||||
|
|
||||||
|
for i := range clusters {
|
||||||
|
c := &clusters[i]
|
||||||
|
proc++
|
||||||
|
|
||||||
|
if c.BastionServer.ID == uuid.Nil || c.BastionServer.Status != "ready" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := log.With().
|
||||||
|
Str("job", jobID).
|
||||||
|
Str("cluster_id", c.ID.String()).
|
||||||
|
Str("cluster_name", c.Name).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
logger.Info().Msg("[cluster_setup] running make setup")
|
||||||
|
|
||||||
|
runCtx, cancel := context.WithTimeout(ctx, perClusterTimeout)
|
||||||
|
out, err := runMakeOnBastion(runCtx, db, c, "ping-servers")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
failCount++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
logger.Error().Err(err).Str("output", out).Msg("[cluster_setup] make ping-servers failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make ping-servers: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusProvisioning, ""); err != nil {
|
||||||
|
failCount++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
logger.Error().Err(err).Msg("[cluster_setup] failed to mark cluster provisioning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
prov++
|
||||||
|
logger.Info().Msg("[cluster_setup] cluster moved to provisioning")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := ClusterSetupResult{
|
||||||
|
Status: "ok",
|
||||||
|
Processed: proc,
|
||||||
|
Provisioning: prov,
|
||||||
|
Failed: failCount,
|
||||||
|
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||||
|
FailedCluster: failedIDs,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("processed", proc).
|
||||||
|
Int("provisioning", prov).
|
||||||
|
Int("failed", failCount).
|
||||||
|
Msg("[cluster_setup] reconcile tick ok")
|
||||||
|
|
||||||
|
// self-reschedule
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"cluster_setup",
|
||||||
|
args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/glueops/autoglue/internal/models"
|
"github.com/glueops/autoglue/internal/models"
|
||||||
"github.com/glueops/autoglue/internal/utils"
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
r53 "github.com/aws/aws-sdk-go-v2/service/route53"
|
r53 "github.com/aws/aws-sdk-go-v2/service/route53"
|
||||||
r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
|
r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
|
||||||
|
"github.com/aws/smithy-go"
|
||||||
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
/************* args & small DTOs *************/
|
/************* args & small DTOs *************/
|
||||||
@@ -47,6 +50,9 @@ const externalDNSPoisonOwner = "autoglue-lock"
|
|||||||
// ExternalDNS poison content – fake owner so real external-dns skips it.
|
// 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"
|
const externalDNSPoisonValue = "heritage=external-dns,external-dns/owner=" + externalDNSPoisonOwner + ",external-dns/resource=manual/autoglue"
|
||||||
|
|
||||||
|
// Default TTL for non-alias records (alias not supported in this reconciler)
|
||||||
|
const defaultRecordTTLSeconds int64 = 300
|
||||||
|
|
||||||
/************* entrypoint worker *************/
|
/************* entrypoint worker *************/
|
||||||
|
|
||||||
func DNSReconsileWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
func DNSReconsileWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
@@ -225,7 +231,14 @@ func processPendingRecordsForDomain(ctx context.Context, db *gorm.DB, d *models.
|
|||||||
applied := 0
|
applied := 0
|
||||||
for i := range records {
|
for i := range records {
|
||||||
if err := applyRecord(ctx, db, r53c, d, &records[i]); err != nil {
|
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")
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("zone_id", d.ZoneID).
|
||||||
|
Str("domain", d.DomainName).
|
||||||
|
Str("record_id", records[i].ID.String()).
|
||||||
|
Str("name", records[i].Name).
|
||||||
|
Str("type", strings.ToUpper(records[i].Type)).
|
||||||
|
Msg("[dns] apply record failed")
|
||||||
_ = setRecordFailed(db, &records[i], err)
|
_ = setRecordFailed(db, &records[i], err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -249,12 +262,24 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
mname := markerName(fq)
|
mname := markerName(fq)
|
||||||
expected := buildMarkerValue(d.OrganizationID.String(), r.ID.String(), r.Fingerprint)
|
expected := buildMarkerValue(d.OrganizationID.String(), r.ID.String(), r.Fingerprint)
|
||||||
|
|
||||||
|
logCtx := log.With().
|
||||||
|
Str("zone_id", zoneID).
|
||||||
|
Str("domain", d.DomainName).
|
||||||
|
Str("fqdn", fq).
|
||||||
|
Str("rr_type", rt).
|
||||||
|
Str("record_id", r.ID.String()).
|
||||||
|
Str("org_id", d.OrganizationID.String()).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
// ---- ExternalDNS preflight ----
|
// ---- ExternalDNS preflight ----
|
||||||
extOwned, err := hasExternalDNSOwnership(ctx, r53c, zoneID, fq, rt)
|
extOwned, err := hasExternalDNSOwnership(ctx, r53c, zoneID, fq, rt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("external_dns_lookup: %w", err)
|
return fmt.Errorf("external_dns_lookup: %w", err)
|
||||||
}
|
}
|
||||||
if extOwned {
|
if extOwned {
|
||||||
|
logCtx.Warn().Msg("[dns] ownership conflict: external-dns claims this record")
|
||||||
r.Owner = "external"
|
r.Owner = "external"
|
||||||
_ = db.Save(r).Error
|
_ = db.Save(r).Error
|
||||||
return fmt.Errorf("ownership_conflict: external-dns claims %s; refusing to modify", strings.TrimSuffix(fq, "."))
|
return fmt.Errorf("ownership_conflict: external-dns claims %s; refusing to modify", strings.TrimSuffix(fq, "."))
|
||||||
@@ -265,6 +290,7 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marker lookup: %w", err)
|
return fmt.Errorf("marker lookup: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasForeignOwner := false
|
hasForeignOwner := false
|
||||||
hasOurExact := false
|
hasOurExact := false
|
||||||
for _, v := range markerVals {
|
for _, v := range markerVals {
|
||||||
@@ -279,25 +305,26 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
hasForeignOwner = true
|
hasForeignOwner = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logCtx.Debug().
|
||||||
|
Bool("externaldns_owned", extOwned).
|
||||||
|
Int("marker_txt_count", len(markerVals)).
|
||||||
|
Bool("marker_has_our_exact", hasOurExact).
|
||||||
|
Bool("marker_has_foreign", hasForeignOwner).
|
||||||
|
Msg("[dns] ownership preflight")
|
||||||
|
|
||||||
if hasForeignOwner {
|
if hasForeignOwner {
|
||||||
|
logCtx.Warn().Msg("[dns] ownership conflict: foreign _autoglue marker")
|
||||||
r.Owner = "external"
|
r.Owner = "external"
|
||||||
_ = db.Save(r).Error
|
_ = db.Save(r).Error
|
||||||
return fmt.Errorf("ownership_conflict: marker for %s is owned by another controller; refusing to modify", strings.TrimSuffix(fq, "."))
|
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
|
// Decode user values
|
||||||
var userVals []string
|
var userVals []string
|
||||||
if len(r.Values) > 0 {
|
rawVals := strings.TrimSpace(string(r.Values))
|
||||||
if err := jsonUnmarshalStrict([]byte(r.Values), &userVals); err != nil {
|
if rawVals != "" && rawVals != "null" {
|
||||||
|
if err := jsonUnmarshalStrict([]byte(rawVals), &userVals); err != nil {
|
||||||
return fmt.Errorf("values decode: %w", err)
|
return fmt.Errorf("values decode: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,15 +333,38 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
recs := make([]r53types.ResourceRecord, 0, len(userVals))
|
recs := make([]r53types.ResourceRecord, 0, len(userVals))
|
||||||
for _, v := range userVals {
|
for _, v := range userVals {
|
||||||
v = strings.TrimSpace(v)
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if rt == "TXT" && !(strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`)) {
|
if rt == "TXT" && !(strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`)) {
|
||||||
v = strconv.Quote(v)
|
v = strconv.Quote(v)
|
||||||
}
|
}
|
||||||
recs = append(recs, r53types.ResourceRecord{Value: aws.String(v)})
|
recs = append(recs, r53types.ResourceRecord{Value: aws.String(v)})
|
||||||
}
|
}
|
||||||
rrChange.ResourceRecordSet.ResourceRecords = recs
|
|
||||||
if r.TTL != nil {
|
// Alias is NOT supported - enforce at least one value for all record types we manage
|
||||||
ttl := int64(*r.TTL)
|
if len(recs) == 0 {
|
||||||
rrChange.ResourceRecordSet.TTL = aws.Int64(ttl)
|
logCtx.Warn().
|
||||||
|
Str("raw_values", truncateForLog(string(r.Values), 240)).
|
||||||
|
Int("decoded_value_count", len(userVals)).
|
||||||
|
Msg("[dns] invalid record: no values (alias not supported)")
|
||||||
|
return fmt.Errorf("invalid_record: %s %s requires at least one value (alias not supported)", strings.TrimSuffix(fq, "."), rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := defaultRecordTTLSeconds
|
||||||
|
if r.TTL != nil && *r.TTL > 0 {
|
||||||
|
ttl = int64(*r.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build RR change (UPSERT)
|
||||||
|
rrChange := r53types.Change{
|
||||||
|
Action: r53types.ChangeActionUpsert,
|
||||||
|
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||||
|
Name: aws.String(fq),
|
||||||
|
Type: r53types.RRType(rt),
|
||||||
|
TTL: aws.Int64(ttl),
|
||||||
|
ResourceRecords: recs,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build marker TXT change (UPSERT)
|
// Build marker TXT change (UPSERT)
|
||||||
@@ -323,7 +373,7 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
ResourceRecordSet: &r53types.ResourceRecordSet{
|
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||||
Name: aws.String(mname),
|
Name: aws.String(mname),
|
||||||
Type: r53types.RRTypeTxt,
|
Type: r53types.RRTypeTxt,
|
||||||
TTL: aws.Int64(300),
|
TTL: aws.Int64(defaultRecordTTLSeconds),
|
||||||
ResourceRecords: []r53types.ResourceRecord{
|
ResourceRecords: []r53types.ResourceRecord{
|
||||||
{Value: aws.String(strconv.Quote(expected))},
|
{Value: aws.String(strconv.Quote(expected))},
|
||||||
},
|
},
|
||||||
@@ -337,14 +387,26 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
changes := []r53types.Change{rrChange, markerChange}
|
changes := []r53types.Change{rrChange, markerChange}
|
||||||
changes = append(changes, poisonChanges...)
|
changes = append(changes, poisonChanges...)
|
||||||
|
|
||||||
|
// Log what we are about to send
|
||||||
|
logCtx.Debug().
|
||||||
|
Interface("route53_change_batch", toLogChangeBatch(zoneID, changes)).
|
||||||
|
Msg("[dns] route53 request preview")
|
||||||
|
|
||||||
_, err = r53c.ChangeResourceRecordSets(ctx, &r53.ChangeResourceRecordSetsInput{
|
_, err = r53c.ChangeResourceRecordSets(ctx, &r53.ChangeResourceRecordSetsInput{
|
||||||
HostedZoneId: aws.String(zoneID),
|
HostedZoneId: aws.String(zoneID),
|
||||||
ChangeBatch: &r53types.ChangeBatch{Changes: changes},
|
ChangeBatch: &r53types.ChangeBatch{Changes: changes},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logAWSError(logCtx, err)
|
||||||
|
logCtx.Info().Dur("elapsed", time.Since(start)).Msg("[dns] apply failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logCtx.Info().
|
||||||
|
Dur("elapsed", time.Since(start)).
|
||||||
|
Int("change_count", len(changes)).
|
||||||
|
Msg("[dns] apply ok")
|
||||||
|
|
||||||
// Success → mark ready & ownership
|
// Success → mark ready & ownership
|
||||||
r.Status = "ready"
|
r.Status = "ready"
|
||||||
r.LastError = ""
|
r.LastError = ""
|
||||||
@@ -352,6 +414,7 @@ func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.D
|
|||||||
if err := db.Save(r).Error; err != nil {
|
if err := db.Save(r).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = hasOurExact // could be used to skip marker write in future
|
_ = hasOurExact // could be used to skip marker write in future
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -568,7 +631,7 @@ func buildExternalDNSPoisonTXTChanges(fqdn, rrType string) []r53types.Change {
|
|||||||
ResourceRecordSet: &r53types.ResourceRecordSet{
|
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||||
Name: aws.String(n),
|
Name: aws.String(n),
|
||||||
Type: r53types.RRTypeTxt,
|
Type: r53types.RRTypeTxt,
|
||||||
TTL: aws.Int64(300),
|
TTL: aws.Int64(defaultRecordTTLSeconds),
|
||||||
ResourceRecords: []r53types.ResourceRecord{
|
ResourceRecords: []r53types.ResourceRecord{
|
||||||
{Value: aws.String(val)},
|
{Value: aws.String(val)},
|
||||||
},
|
},
|
||||||
@@ -595,3 +658,125 @@ func jsonUnmarshalStrict(b []byte, dst any) error {
|
|||||||
}
|
}
|
||||||
return json.Unmarshal(b, dst)
|
return json.Unmarshal(b, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* logging DTOs & helpers *************/
|
||||||
|
|
||||||
|
type logRR struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type logRRSet struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TTL *int64 `json:"ttl,omitempty"`
|
||||||
|
Records []logRR `json:"records,omitempty"`
|
||||||
|
RecordCount int `json:"record_count"`
|
||||||
|
HasAliasTarget bool `json:"has_alias_target"`
|
||||||
|
SetIdentifier *string `json:"set_identifier,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type logChangeBatch struct {
|
||||||
|
HostedZoneID string `json:"hosted_zone_id"`
|
||||||
|
ChangeCount int `json:"change_count"`
|
||||||
|
Changes []logRRSet `json:"changes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateForLog(s string, max int) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if max <= 0 || len(s) <= max {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:max] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLogChangeBatch(zoneID string, changes []r53types.Change) logChangeBatch {
|
||||||
|
out := logChangeBatch{
|
||||||
|
HostedZoneID: zoneID,
|
||||||
|
ChangeCount: len(changes),
|
||||||
|
Changes: make([]logRRSet, 0, len(changes)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range changes {
|
||||||
|
if ch.ResourceRecordSet == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rrs := ch.ResourceRecordSet
|
||||||
|
lc := logRRSet{
|
||||||
|
Action: string(ch.Action),
|
||||||
|
Name: aws.ToString(rrs.Name),
|
||||||
|
Type: string(rrs.Type),
|
||||||
|
TTL: rrs.TTL,
|
||||||
|
HasAliasTarget: rrs.AliasTarget != nil,
|
||||||
|
SetIdentifier: rrs.SetIdentifier,
|
||||||
|
RecordCount: len(rrs.ResourceRecords),
|
||||||
|
Records: make([]logRR, 0, min(len(rrs.ResourceRecords), 5)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log up to first 5 values (truncate each) to avoid log bloat / secrets
|
||||||
|
for i, rr := range rrs.ResourceRecords {
|
||||||
|
if i >= 5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lc.Records = append(lc.Records, logRR{Value: truncateForLog(aws.ToString(rr.Value), 160)})
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Changes = append(out.Changes, lc)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// logAWSError extracts useful smithy/HTTP metadata (status code + request id + api code) into logs.
|
||||||
|
// logAWSError extracts useful smithy/HTTP metadata (status code + request id + api code) into logs.
|
||||||
|
func logAWSError(l zerolog.Logger, err error) {
|
||||||
|
// Add operation context if present
|
||||||
|
var opErr *smithy.OperationError
|
||||||
|
if errors.As(err, &opErr) {
|
||||||
|
l = l.With().
|
||||||
|
Str("aws_service", opErr.ServiceID).
|
||||||
|
Str("aws_operation", opErr.OperationName).
|
||||||
|
Logger()
|
||||||
|
err = opErr.Unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP status + request id (smithy-go transport/http)
|
||||||
|
var re *smithyhttp.ResponseError
|
||||||
|
if errors.As(err, &re) {
|
||||||
|
status := re.HTTPStatusCode()
|
||||||
|
|
||||||
|
reqID := ""
|
||||||
|
if resp := re.HTTPResponse(); resp != nil && resp.Header != nil {
|
||||||
|
reqID = resp.Header.Get("x-amzn-RequestId")
|
||||||
|
if reqID == "" {
|
||||||
|
reqID = resp.Header.Get("x-amz-request-id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev := l.Error().Int("http_status", status).Err(err)
|
||||||
|
if reqID != "" {
|
||||||
|
ev = ev.Str("aws_request_id", reqID)
|
||||||
|
}
|
||||||
|
ev.Msg("[dns] aws route53 call failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// API error code/message (best-effort)
|
||||||
|
var apiErr smithy.APIError
|
||||||
|
if errors.As(err, &apiErr) {
|
||||||
|
l.Error().
|
||||||
|
Str("aws_error_code", apiErr.ErrorCode()).
|
||||||
|
Str("aws_error_message", apiErr.ErrorMessage()).
|
||||||
|
Err(err).
|
||||||
|
Msg("[dns] aws route53 api error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Error().Err(err).Msg("[dns] aws route53 error")
|
||||||
|
}
|
||||||
|
|||||||
95
internal/bg/org_key_sweeper.go
Normal file
95
internal/bg/org_key_sweeper.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrgKeySweeperArgs struct {
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
RetentionDays int `json:"retention_days,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrgKeySweeperResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
MarkedRevoked int `json:"marked_revoked"`
|
||||||
|
DeletedEphemeral int `json:"deleted_ephemeral"`
|
||||||
|
ElapsedMs int `json:"elapsed_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrgKeySweeperWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := OrgKeySweeperArgs{
|
||||||
|
IntervalS: 3600,
|
||||||
|
RetentionDays: 10,
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 3600
|
||||||
|
}
|
||||||
|
if args.RetentionDays <= 0 {
|
||||||
|
args.RetentionDays = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 1) Mark expired keys as revoked
|
||||||
|
res1 := db.Model(&models.APIKey{}).
|
||||||
|
Where("expires_at IS NOT NULL AND expires_at <= ? AND revoked = false", now).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"revoked": true,
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if res1.Error != nil {
|
||||||
|
log.Error().Err(res1.Error).Msg("[org_key_sweeper] mark expired revoked failed")
|
||||||
|
return nil, res1.Error
|
||||||
|
}
|
||||||
|
markedRevoked := int(res1.RowsAffected)
|
||||||
|
|
||||||
|
// 2) Hard-delete ephemeral keys that are revoked and older than retention
|
||||||
|
cutoff := now.Add(-time.Duration(args.RetentionDays) * 24 * time.Hour)
|
||||||
|
res2 := db.
|
||||||
|
Where("is_ephemeral = ? AND revoked = ? AND updated_at <= ?", true, true, cutoff).
|
||||||
|
Delete(&models.APIKey{})
|
||||||
|
|
||||||
|
if res2.Error != nil {
|
||||||
|
log.Error().Err(res2.Error).Msg("[org_key_sweeper] delete revoked ephemeral keys failed")
|
||||||
|
return nil, res2.Error
|
||||||
|
}
|
||||||
|
deletedEphemeral := int(res2.RowsAffected)
|
||||||
|
|
||||||
|
out := OrgKeySweeperResult{
|
||||||
|
Status: "ok",
|
||||||
|
MarkedRevoked: markedRevoked,
|
||||||
|
DeletedEphemeral: deletedEphemeral,
|
||||||
|
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("marked_revoked", markedRevoked).
|
||||||
|
Int("deleted_ephemeral", deletedEphemeral).
|
||||||
|
Msg("[org_key_sweeper] cleanup tick ok")
|
||||||
|
|
||||||
|
// Re-enqueue the sweeper
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"org_key_sweeper",
|
||||||
|
args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
658
internal/bg/prepare_cluster.go
Normal file
658
internal/bg/prepare_cluster.go
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/mapper"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterPrepareArgs struct {
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterPrepareFailure struct {
|
||||||
|
ClusterID uuid.UUID `json:"cluster_id"`
|
||||||
|
Step string `json:"step"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterPrepareResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Processed int `json:"processed"`
|
||||||
|
MarkedPending int `json:"marked_pending"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
ElapsedMs int `json:"elapsed_ms"`
|
||||||
|
FailedIDs []uuid.UUID `json:"failed_cluster_ids"`
|
||||||
|
Failures []ClusterPrepareFailure `json:"failures"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias the status constants from models to avoid string drift.
|
||||||
|
const (
|
||||||
|
clusterStatusPrePending = models.ClusterStatusPrePending
|
||||||
|
clusterStatusPending = models.ClusterStatusPending
|
||||||
|
clusterStatusProvisioning = models.ClusterStatusProvisioning
|
||||||
|
clusterStatusReady = models.ClusterStatusReady
|
||||||
|
clusterStatusFailed = models.ClusterStatusFailed
|
||||||
|
clusterStatusBootstrapping = models.ClusterStatusBootstrapping
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClusterPrepareWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := ClusterPrepareArgs{IntervalS: 120}
|
||||||
|
jobID := j.ID
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 120
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all clusters that are pre_pending; we’ll filter for bastion.ready in memory.
|
||||||
|
var clusters []models.Cluster
|
||||||
|
if err := db.
|
||||||
|
Preload("BastionServer.SshKey").
|
||||||
|
Preload("CaptainDomain").
|
||||||
|
Preload("ControlPlaneRecordSet").
|
||||||
|
Preload("AppsLoadBalancer").
|
||||||
|
Preload("GlueOpsLoadBalancer").
|
||||||
|
Preload("NodePools").
|
||||||
|
Preload("NodePools.Labels").
|
||||||
|
Preload("NodePools.Annotations").
|
||||||
|
Preload("NodePools.Taints").
|
||||||
|
Preload("NodePools.Servers.SshKey").
|
||||||
|
Where("status = ?", clusterStatusPrePending).
|
||||||
|
Find(&clusters).Error; err != nil {
|
||||||
|
log.Error().Err(err).Msg("[cluster_prepare] query clusters failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, ok, fail := 0, 0, 0
|
||||||
|
var failedIDs []uuid.UUID
|
||||||
|
var failures []ClusterPrepareFailure
|
||||||
|
|
||||||
|
perClusterTimeout := 8 * time.Minute
|
||||||
|
|
||||||
|
for i := range clusters {
|
||||||
|
c := &clusters[i]
|
||||||
|
proc++
|
||||||
|
|
||||||
|
// bastion must exist and be ready
|
||||||
|
if c.BastionServer == nil || c.BastionServerID == nil || *c.BastionServerID == uuid.Nil || c.BastionServer.Status != "ready" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusBootstrapping, ""); err != nil {
|
||||||
|
log.Error().Err(err).Msg("[cluster_prepare] failed to mark cluster bootstrapping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status = clusterStatusBootstrapping
|
||||||
|
|
||||||
|
clusterLog := log.With().
|
||||||
|
Str("job", jobID).
|
||||||
|
Str("cluster_id", c.ID.String()).
|
||||||
|
Str("cluster_name", c.Name).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
clusterLog.Info().Msg("[cluster_prepare] starting")
|
||||||
|
|
||||||
|
if err := validateClusterForPrepare(c); err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "validate",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] validation failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allServers := flattenClusterServers(c)
|
||||||
|
keyPayloads, sshConfig, err := buildSSHAssetsForCluster(db, c, allServers)
|
||||||
|
if err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "build_ssh_assets",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] build ssh assets failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dtoCluster := mapper.ClusterToDTO(*c)
|
||||||
|
|
||||||
|
if c.EncryptedKubeconfig != "" && c.KubeIV != "" && c.KubeTag != "" {
|
||||||
|
kubeconfig, err := utils.DecryptForOrg(
|
||||||
|
c.OrganizationID,
|
||||||
|
c.EncryptedKubeconfig,
|
||||||
|
c.KubeIV,
|
||||||
|
c.KubeTag,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "decrypt_kubeconfig",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] decrypt kubeconfig failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dtoCluster.Kubeconfig = &kubeconfig
|
||||||
|
}
|
||||||
|
|
||||||
|
orgKey, orgSecret, err := findOrCreateClusterAutomationKey(
|
||||||
|
db,
|
||||||
|
c.OrganizationID,
|
||||||
|
c.ID,
|
||||||
|
24*time.Hour,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "create_org_key",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] create org key for payload failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dtoCluster.OrgKey = &orgKey
|
||||||
|
dtoCluster.OrgSecret = &orgSecret
|
||||||
|
|
||||||
|
payloadJSON, err := json.MarshalIndent(dtoCluster, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "marshal_payload",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] json marshal failed")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
runCtx, cancel := context.WithTimeout(ctx, perClusterTimeout)
|
||||||
|
err = pushAssetsToBastion(runCtx, db, c, sshConfig, keyPayloads, payloadJSON)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "ssh_push",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] failed to push assets to bastion")
|
||||||
|
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setClusterStatus(db, c.ID, clusterStatusPending, ""); err != nil {
|
||||||
|
fail++
|
||||||
|
failedIDs = append(failedIDs, c.ID)
|
||||||
|
failures = append(failures, ClusterPrepareFailure{
|
||||||
|
ClusterID: c.ID,
|
||||||
|
Step: "set_pending",
|
||||||
|
Reason: err.Error(),
|
||||||
|
})
|
||||||
|
clusterLog.Error().Err(err).Msg("[cluster_prepare] failed to mark cluster pending")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ok++
|
||||||
|
clusterLog.Info().Msg("[cluster_prepare] cluster marked pending")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := ClusterPrepareResult{
|
||||||
|
Status: "ok",
|
||||||
|
Processed: proc,
|
||||||
|
MarkedPending: ok,
|
||||||
|
Failed: fail,
|
||||||
|
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||||
|
FailedIDs: failedIDs,
|
||||||
|
Failures: failures,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("processed", proc).
|
||||||
|
Int("pending", ok).
|
||||||
|
Int("failed", fail).
|
||||||
|
Msg("[cluster_prepare] reconcile tick ok")
|
||||||
|
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"prepare_cluster",
|
||||||
|
args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- helpers ----------
|
||||||
|
|
||||||
|
func validateClusterForPrepare(c *models.Cluster) error {
|
||||||
|
if c.BastionServer == nil || c.BastionServerID == nil || *c.BastionServerID == uuid.Nil {
|
||||||
|
return fmt.Errorf("missing bastion server")
|
||||||
|
}
|
||||||
|
if c.BastionServer.Status != "ready" {
|
||||||
|
return fmt.Errorf("bastion server not ready (status=%s)", c.BastionServer.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptainDomain is a value type; presence is via *ID
|
||||||
|
if c.CaptainDomainID == nil || *c.CaptainDomainID == uuid.Nil {
|
||||||
|
return fmt.Errorf("missing captain domain for cluster")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ControlPlaneRecordSet is a pointer; presence is via *ID + non-nil struct
|
||||||
|
if c.ControlPlaneRecordSetID == nil || *c.ControlPlaneRecordSetID == uuid.Nil || c.ControlPlaneRecordSet == nil {
|
||||||
|
return fmt.Errorf("missing control_plane_record_set for cluster")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.NodePools) == 0 {
|
||||||
|
return fmt.Errorf("cluster has no node pools")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasServer := false
|
||||||
|
for i := range c.NodePools {
|
||||||
|
if len(c.NodePools[i].Servers) > 0 {
|
||||||
|
hasServer = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasServer {
|
||||||
|
return fmt.Errorf("cluster has no servers attached to node pools")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenClusterServers(c *models.Cluster) []*models.Server {
|
||||||
|
var out []*models.Server
|
||||||
|
for i := range c.NodePools {
|
||||||
|
for j := range c.NodePools[i].Servers {
|
||||||
|
s := &c.NodePools[i].Servers[j]
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyPayload struct {
|
||||||
|
FileName string
|
||||||
|
PrivateKeyB64 string
|
||||||
|
}
|
||||||
|
|
||||||
|
// build ssh-config for all servers + decrypt keys.
|
||||||
|
// ssh-config is intended to live on the bastion and connect via *private* IPs.
|
||||||
|
func buildSSHAssetsForCluster(db *gorm.DB, c *models.Cluster, servers []*models.Server) (map[uuid.UUID]keyPayload, string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
keys := make(map[uuid.UUID]keyPayload)
|
||||||
|
|
||||||
|
for _, s := range servers {
|
||||||
|
// Defensive checks
|
||||||
|
if strings.TrimSpace(s.PrivateIPAddress) == "" {
|
||||||
|
return nil, "", fmt.Errorf("server %s missing private ip", s.ID)
|
||||||
|
}
|
||||||
|
if s.SshKeyID == uuid.Nil {
|
||||||
|
return nil, "", fmt.Errorf("server %s missing ssh key relation", s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// de-dupe keys: many servers may share the same ssh key
|
||||||
|
if _, ok := keys[s.SshKeyID]; !ok {
|
||||||
|
priv, err := utils.DecryptForOrg(
|
||||||
|
s.OrganizationID,
|
||||||
|
s.SshKey.EncryptedPrivateKey,
|
||||||
|
s.SshKey.PrivateIV,
|
||||||
|
s.SshKey.PrivateTag,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("decrypt key for server %s: %w", s.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fname := fmt.Sprintf("%s.pem", s.SshKeyID.String())
|
||||||
|
keys[s.SshKeyID] = keyPayload{
|
||||||
|
FileName: fname,
|
||||||
|
PrivateKeyB64: base64.StdEncoding.EncodeToString([]byte(priv)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ssh config entry per server
|
||||||
|
keyFile := keys[s.SshKeyID].FileName
|
||||||
|
|
||||||
|
hostAlias := s.Hostname
|
||||||
|
if hostAlias == "" {
|
||||||
|
hostAlias = s.ID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("Host %s\n", hostAlias))
|
||||||
|
sb.WriteString(fmt.Sprintf(" HostName %s\n", s.PrivateIPAddress))
|
||||||
|
sb.WriteString(fmt.Sprintf(" User %s\n", s.SSHUser))
|
||||||
|
sb.WriteString(fmt.Sprintf(" IdentityFile ~/.ssh/autoglue/keys/%s\n", keyFile))
|
||||||
|
sb.WriteString(" IdentitiesOnly yes\n")
|
||||||
|
sb.WriteString(" StrictHostKeyChecking accept-new\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushAssetsToBastion(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
c *models.Cluster,
|
||||||
|
sshConfig string,
|
||||||
|
keyPayloads map[uuid.UUID]keyPayload,
|
||||||
|
payloadJSON []byte,
|
||||||
|
) error {
|
||||||
|
bastion := c.BastionServer
|
||||||
|
if bastion == nil {
|
||||||
|
return fmt.Errorf("bastion server is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bastion.PublicIPAddress == nil || strings.TrimSpace(*bastion.PublicIPAddress) == "" {
|
||||||
|
return fmt.Errorf("bastion server missing public ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := utils.DecryptForOrg(
|
||||||
|
bastion.OrganizationID,
|
||||||
|
bastion.SshKey.EncryptedPrivateKey,
|
||||||
|
bastion.SshKey.PrivateIV,
|
||||||
|
bastion.SshKey.PrivateTag,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt bastion key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ssh.ParsePrivateKey([]byte(privKey))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse bastion private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hkcb := makeDBHostKeyCallback(db, bastion)
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: bastion.SSHUser,
|
||||||
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||||
|
HostKeyCallback: hkcb,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
host := net.JoinHostPort(*bastion.PublicIPAddress, "22")
|
||||||
|
|
||||||
|
dialer := &net.Dialer{}
|
||||||
|
conn, err := dialer.DialContext(ctx, "tcp", host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial bastion: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
cconn, chans, reqs, err := ssh.NewClientConn(conn, host, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ssh handshake bastion: %w", err)
|
||||||
|
}
|
||||||
|
client := ssh.NewClient(cconn, chans, reqs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
sess, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ssh session: %w", err)
|
||||||
|
}
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
// build one shot script to:
|
||||||
|
// - mkdir ~/.ssh/autoglue/keys
|
||||||
|
// - write cluster-specific ssh-config
|
||||||
|
// - write all private keys
|
||||||
|
// - write payload.json
|
||||||
|
clusterDir := fmt.Sprintf("$HOME/autoglue/clusters/%s", c.ID.String())
|
||||||
|
configPath := fmt.Sprintf("$HOME/.ssh/autoglue/cluster-%s.config", c.ID.String())
|
||||||
|
|
||||||
|
var script bytes.Buffer
|
||||||
|
|
||||||
|
script.WriteString("set -euo pipefail\n")
|
||||||
|
script.WriteString("mkdir -p \"$HOME/.ssh/autoglue/keys\"\n")
|
||||||
|
script.WriteString("mkdir -p " + clusterDir + "\n")
|
||||||
|
script.WriteString("chmod 700 \"$HOME/.ssh\" || true\n")
|
||||||
|
|
||||||
|
// ssh-config
|
||||||
|
script.WriteString("cat > " + configPath + " <<'EOF_CFG'\n")
|
||||||
|
script.WriteString(sshConfig)
|
||||||
|
script.WriteString("EOF_CFG\n")
|
||||||
|
script.WriteString("chmod 600 " + configPath + "\n")
|
||||||
|
|
||||||
|
// keys
|
||||||
|
for id, kp := range keyPayloads {
|
||||||
|
tag := "KEY_" + id.String()
|
||||||
|
target := fmt.Sprintf("$HOME/.ssh/autoglue/keys/%s", kp.FileName)
|
||||||
|
|
||||||
|
script.WriteString("cat <<'" + tag + "' | base64 -d > " + target + "\n")
|
||||||
|
script.WriteString(kp.PrivateKeyB64 + "\n")
|
||||||
|
script.WriteString(tag + "\n")
|
||||||
|
script.WriteString("chmod 600 " + target + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// payload.json
|
||||||
|
payloadPath := clusterDir + "/payload.json"
|
||||||
|
script.WriteString("cat > " + payloadPath + " <<'EOF_PAYLOAD'\n")
|
||||||
|
script.Write(payloadJSON)
|
||||||
|
script.WriteString("\nEOF_PAYLOAD\n")
|
||||||
|
script.WriteString("chmod 600 " + payloadPath + "\n")
|
||||||
|
|
||||||
|
// If you later want to always include cluster configs automatically, you can
|
||||||
|
// optionally manage ~/.ssh/config here (kept simple for now).
|
||||||
|
|
||||||
|
sess.Stdin = strings.NewReader(script.String())
|
||||||
|
out, runErr := sess.CombinedOutput("bash -s")
|
||||||
|
|
||||||
|
if runErr != nil {
|
||||||
|
return wrapSSHError(runErr, string(out))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setClusterStatus(db *gorm.DB, id uuid.UUID, status, lastError string) error {
|
||||||
|
updates := map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
if lastError != "" {
|
||||||
|
updates["last_error"] = lastError
|
||||||
|
}
|
||||||
|
return db.Model(&models.Cluster{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// runMakeOnBastion runs `make <target>` from the cluster's directory on the bastion.
|
||||||
|
func runMakeOnBastion(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
c *models.Cluster,
|
||||||
|
target string,
|
||||||
|
) (string, error) {
|
||||||
|
logger := log.With().
|
||||||
|
Str("cluster_id", c.ID.String()).
|
||||||
|
Str("cluster_name", c.Name).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
bastion := c.BastionServer
|
||||||
|
if bastion == nil {
|
||||||
|
return "", fmt.Errorf("bastion server is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bastion.PublicIPAddress == nil || strings.TrimSpace(*bastion.PublicIPAddress) == "" {
|
||||||
|
return "", fmt.Errorf("bastion server missing public ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := utils.DecryptForOrg(
|
||||||
|
bastion.OrganizationID,
|
||||||
|
bastion.SshKey.EncryptedPrivateKey,
|
||||||
|
bastion.SshKey.PrivateIV,
|
||||||
|
bastion.SshKey.PrivateTag,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decrypt bastion key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ssh.ParsePrivateKey([]byte(privKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse bastion private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hkcb := makeDBHostKeyCallback(db, bastion)
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: bastion.SSHUser,
|
||||||
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||||
|
HostKeyCallback: hkcb,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
host := net.JoinHostPort(*bastion.PublicIPAddress, "22")
|
||||||
|
|
||||||
|
dialer := &net.Dialer{}
|
||||||
|
conn, err := dialer.DialContext(ctx, "tcp", host)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("dial bastion: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
cconn, chans, reqs, err := ssh.NewClientConn(conn, host, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ssh handshake bastion: %w", err)
|
||||||
|
}
|
||||||
|
client := ssh.NewClient(cconn, chans, reqs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
sess, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ssh session: %w", err)
|
||||||
|
}
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
clusterDir := fmt.Sprintf("$HOME/autoglue/clusters/%s", c.ID.String())
|
||||||
|
sshDir := fmt.Sprintf("$HOME/.ssh")
|
||||||
|
|
||||||
|
cmd := fmt.Sprintf("cd %s && docker run -v %s:/root/.ssh -v ./payload.json:/opt/gluekube/platform.json %s:%s make %s", clusterDir, sshDir, c.DockerImage, c.DockerTag, target)
|
||||||
|
|
||||||
|
logger.Info().
|
||||||
|
Str("cmd", cmd).
|
||||||
|
Msg("[runMakeOnBastion] executing remote command")
|
||||||
|
|
||||||
|
out, runErr := sess.CombinedOutput(cmd)
|
||||||
|
if runErr != nil {
|
||||||
|
return string(out), wrapSSHError(runErr, string(out))
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomB64URL(n int) (string, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOrCreateClusterAutomationKey(
|
||||||
|
db *gorm.DB,
|
||||||
|
orgID uuid.UUID,
|
||||||
|
clusterID uuid.UUID,
|
||||||
|
ttl time.Duration,
|
||||||
|
) (orgKey string, orgSecret string, err error) {
|
||||||
|
now := time.Now()
|
||||||
|
name := fmt.Sprintf("cluster-%s-bastion", clusterID.String())
|
||||||
|
|
||||||
|
// 1) Delete any existing ephemeral cluster-bastion key for this org+cluster
|
||||||
|
if err := db.Where(
|
||||||
|
"org_id = ? AND scope = ? AND purpose = ? AND cluster_id = ? AND is_ephemeral = ?",
|
||||||
|
orgID, "org", "cluster_bastion", clusterID, true,
|
||||||
|
).Delete(&models.APIKey{}).Error; err != nil {
|
||||||
|
return "", "", fmt.Errorf("delete existing cluster key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Mint a fresh keypair
|
||||||
|
keySuffix, err := randomB64URL(16)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("entropy_error: %w", err)
|
||||||
|
}
|
||||||
|
sec, err := randomB64URL(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("entropy_error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
orgKey = "org_" + keySuffix
|
||||||
|
orgSecret = sec
|
||||||
|
|
||||||
|
keyHash := auth.SHA256Hex(orgKey)
|
||||||
|
secretHash, err := auth.HashSecretArgon2id(orgSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("hash_error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exp := now.Add(ttl)
|
||||||
|
|
||||||
|
prefix := orgKey
|
||||||
|
if len(prefix) > 12 {
|
||||||
|
prefix = prefix[:12]
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := models.APIKey{
|
||||||
|
OrgID: &orgID,
|
||||||
|
Scope: "org",
|
||||||
|
Purpose: "cluster_bastion",
|
||||||
|
ClusterID: &clusterID,
|
||||||
|
IsEphemeral: true,
|
||||||
|
Name: name,
|
||||||
|
KeyHash: keyHash,
|
||||||
|
SecretHash: &secretHash,
|
||||||
|
ExpiresAt: &exp,
|
||||||
|
Revoked: false,
|
||||||
|
Prefix: &prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
|
return "", "", fmt.Errorf("db_error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgKey, orgSecret, nil
|
||||||
|
}
|
||||||
256
internal/handlers/actions.go
Normal file
256
internal/handlers/actions.go
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListActions godoc
|
||||||
|
//
|
||||||
|
// @ID ListActions
|
||||||
|
// @Summary List available actions
|
||||||
|
// @Description Returns all admin-configured actions.
|
||||||
|
// @Tags Actions
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} dto.ActionResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /admin/actions [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func ListActions(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var rows []models.Action
|
||||||
|
if err := db.Order("label ASC").Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]dto.ActionResponse, 0, len(rows))
|
||||||
|
for _, a := range rows {
|
||||||
|
out = append(out, actionToDTO(a))
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAction godoc
|
||||||
|
//
|
||||||
|
// @ID GetAction
|
||||||
|
// @Summary Get a single action by ID
|
||||||
|
// @Description Returns a single action.
|
||||||
|
// @Tags Actions
|
||||||
|
// @Produce json
|
||||||
|
// @Param actionID path string true "Action ID"
|
||||||
|
// @Success 200 {object} dto.ActionResponse
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /admin/actions/{actionID} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func GetAction(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.Action
|
||||||
|
if err := db.Where("id = ?", actionID).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "action not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, actionToDTO(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAction godoc
|
||||||
|
//
|
||||||
|
// @ID CreateAction
|
||||||
|
// @Summary Create an action
|
||||||
|
// @Description Creates a new admin-configured action.
|
||||||
|
// @Tags Actions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body dto.CreateActionRequest true "payload"
|
||||||
|
// @Success 201 {object} dto.ActionResponse
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /admin/actions [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func CreateAction(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var in dto.CreateActionRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
label := strings.TrimSpace(in.Label)
|
||||||
|
desc := strings.TrimSpace(in.Description)
|
||||||
|
target := strings.TrimSpace(in.MakeTarget)
|
||||||
|
|
||||||
|
if label == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "label is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if desc == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "description is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if target == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "make_target is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
row := models.Action{
|
||||||
|
Label: label,
|
||||||
|
Description: desc,
|
||||||
|
MakeTarget: target,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, actionToDTO(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAction godoc
|
||||||
|
//
|
||||||
|
// @ID UpdateAction
|
||||||
|
// @Summary Update an action
|
||||||
|
// @Description Updates an action. Only provided fields are modified.
|
||||||
|
// @Tags Actions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param actionID path string true "Action ID"
|
||||||
|
// @Param body body dto.UpdateActionRequest true "payload"
|
||||||
|
// @Success 200 {object} dto.ActionResponse
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /admin/actions/{actionID} [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func UpdateAction(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in dto.UpdateActionRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.Action
|
||||||
|
if err := db.Where("id = ?", actionID).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "action not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Label != nil {
|
||||||
|
v := strings.TrimSpace(*in.Label)
|
||||||
|
if v == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "label cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.Label = v
|
||||||
|
}
|
||||||
|
if in.Description != nil {
|
||||||
|
v := strings.TrimSpace(*in.Description)
|
||||||
|
if v == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "description cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.Description = v
|
||||||
|
}
|
||||||
|
if in.MakeTarget != nil {
|
||||||
|
v := strings.TrimSpace(*in.MakeTarget)
|
||||||
|
if v == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "validation_error", "make_target cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.MakeTarget = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Save(&row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, actionToDTO(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAction godoc
|
||||||
|
//
|
||||||
|
// @ID DeleteAction
|
||||||
|
// @Summary Delete an action
|
||||||
|
// @Description Deletes an action.
|
||||||
|
// @Tags Actions
|
||||||
|
// @Produce json
|
||||||
|
// @Param actionID path string true "Action ID"
|
||||||
|
// @Success 204 {string} string "deleted"
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /admin/actions/{actionID} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func DeleteAction(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := db.Where("id = ?", actionID).Delete(&models.Action{})
|
||||||
|
if tx.Error != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "action not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionToDTO(a models.Action) dto.ActionResponse {
|
||||||
|
return dto.ActionResponse{
|
||||||
|
ID: a.ID,
|
||||||
|
Label: a.Label,
|
||||||
|
Description: a.Description,
|
||||||
|
MakeTarget: a.MakeTarget,
|
||||||
|
CreatedAt: a.CreatedAt,
|
||||||
|
UpdatedAt: a.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
// @Summary List annotations (org scoped)
|
// @Summary List annotations (org scoped)
|
||||||
// @Description Returns annotations for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.
|
// @Description Returns annotations for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.
|
||||||
// @Tags Annotations
|
// @Tags Annotations
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param key query string false "Exact key"
|
// @Param key query string false "Exact key"
|
||||||
@@ -75,7 +74,6 @@ func ListAnnotations(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Get annotation by ID (org scoped)
|
// @Summary Get annotation by ID (org scoped)
|
||||||
// @Description Returns one annotation. Add `include=node_pools` to include node pools.
|
// @Description Returns one annotation. Add `include=node_pools` to include node pools.
|
||||||
// @Tags Annotations
|
// @Tags Annotations
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Annotation ID (UUID)"
|
// @Param id path string true "Annotation ID (UUID)"
|
||||||
@@ -255,11 +253,10 @@ func UpdateAnnotation(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Delete annotation (org scoped)
|
// @Summary Delete annotation (org scoped)
|
||||||
// @Description Permanently deletes the annotation.
|
// @Description Permanently deletes the annotation.
|
||||||
// @Tags Annotations
|
// @Tags Annotations
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Annotation ID (UUID)"
|
// @Param id path string true "Annotation ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -252,10 +254,11 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
|||||||
accessTTL := 1 * time.Hour
|
accessTTL := 1 * time.Hour
|
||||||
refreshTTL := 30 * 24 * time.Hour
|
refreshTTL := 30 * 24 * time.Hour
|
||||||
|
|
||||||
|
cfgLoaded, _ := config.Load()
|
||||||
access, err := auth.IssueAccessToken(auth.IssueOpts{
|
access, err := auth.IssueAccessToken(auth.IssueOpts{
|
||||||
Subject: user.ID.String(),
|
Subject: user.ID.String(),
|
||||||
Issuer: cfg.JWTIssuer,
|
Issuer: cfgLoaded.JWTIssuer,
|
||||||
Audience: cfg.JWTAudience,
|
Audience: cfgLoaded.JWTAudience,
|
||||||
TTL: accessTTL,
|
TTL: accessTTL,
|
||||||
Claims: map[string]any{
|
Claims: map[string]any{
|
||||||
"email": email,
|
"email": email,
|
||||||
@@ -273,7 +276,10 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
secure := true
|
||||||
|
if u, err := url.Parse(cfg.OAuthRedirectBase); err == nil && isLocalDev(u) {
|
||||||
|
secure = false
|
||||||
|
}
|
||||||
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||||
secure = strings.EqualFold(xf, "https")
|
secure = strings.EqualFold(xf, "https")
|
||||||
}
|
}
|
||||||
@@ -291,14 +297,7 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
|||||||
// If the state indicates SPA popup mode, postMessage tokens to the opener and close
|
// If the state indicates SPA popup mode, postMessage tokens to the opener and close
|
||||||
state := r.URL.Query().Get("state")
|
state := r.URL.Query().Get("state")
|
||||||
if strings.Contains(state, "mode=spa") {
|
if strings.Contains(state, "mode=spa") {
|
||||||
origin := ""
|
origin := canonicalOrigin(cfg.OAuthRedirectBase)
|
||||||
for _, part := range strings.Split(state, "|") {
|
|
||||||
if strings.HasPrefix(part, "origin=") {
|
|
||||||
origin, _ = url.QueryUnescape(strings.TrimPrefix(part, "origin="))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// fallback: restrict to backend origin if none supplied
|
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
origin = cfg.OAuthRedirectBase
|
origin = cfg.OAuthRedirectBase
|
||||||
}
|
}
|
||||||
@@ -371,7 +370,10 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
secure := true
|
||||||
|
if uParsed, err := url.Parse(cfg.OAuthRedirectBase); err == nil && isLocalDev(uParsed) {
|
||||||
|
secure = false
|
||||||
|
}
|
||||||
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||||
secure = strings.EqualFold(xf, "https")
|
secure = strings.EqualFold(xf, "https")
|
||||||
}
|
}
|
||||||
@@ -424,6 +426,11 @@ func Logout(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearCookie:
|
clearCookie:
|
||||||
|
secure := true
|
||||||
|
if uParsed, err := url.Parse(cfg.OAuthRedirectBase); err == nil && isLocalDev(uParsed) {
|
||||||
|
secure = false
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "ag_jwt",
|
Name: "ag_jwt",
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -432,11 +439,10 @@ func Logout(db *gorm.DB) http.HandlerFunc {
|
|||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Expires: time.Unix(0, 0),
|
Expires: time.Unix(0, 0),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
Secure: strings.HasPrefix(cfg.OAuthRedirectBase, "https"),
|
Secure: secure,
|
||||||
})
|
})
|
||||||
|
|
||||||
w.WriteHeader(204)
|
w.WriteHeader(204)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,21 +512,63 @@ func ensureAutoMembership(db *gorm.DB, userID uuid.UUID, email string) error {
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// postMessage HTML template
|
||||||
|
var postMessageTpl = template.Must(template.New("postmsg").Parse(`<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(atob("{{.PayloadB64}}"));
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.postMessage(
|
||||||
|
{ type: 'autoglue:auth', payload: data },
|
||||||
|
"{{.Origin}}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
window.close();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
|
|
||||||
|
type postMessageData struct {
|
||||||
|
Origin string
|
||||||
|
PayloadB64 string
|
||||||
|
}
|
||||||
|
|
||||||
// writePostMessageHTML sends a tiny HTML page that posts tokens to the SPA and closes the window.
|
// writePostMessageHTML sends a tiny HTML page that posts tokens to the SPA and closes the window.
|
||||||
func writePostMessageHTML(w http.ResponseWriter, origin string, payload dto.TokenPair) {
|
func writePostMessageHTML(w http.ResponseWriter, origin string, payload dto.TokenPair) {
|
||||||
b, _ := json.Marshal(payload)
|
b, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
data := postMessageData{
|
||||||
|
Origin: origin,
|
||||||
|
PayloadB64: base64.StdEncoding.EncodeToString(b),
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write([]byte(`<!doctype html><html><body><script>
|
_ = postMessageTpl.Execute(w, data)
|
||||||
(function(){
|
}
|
||||||
try {
|
|
||||||
var data = ` + string(b) + `;
|
// canonicalOrigin returns scheme://host[:port] for a given URL, or "" if invalid.
|
||||||
if (window.opener) {
|
func canonicalOrigin(raw string) string {
|
||||||
window.opener.postMessage({ type: 'autoglue:auth', payload: data }, '` + origin + `');
|
u, err := url.Parse(raw)
|
||||||
}
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||||
} catch (e) {}
|
return ""
|
||||||
window.close();
|
}
|
||||||
})();
|
|
||||||
</script></body></html>`))
|
// Normalize: no path/query/fragment — just the origin.
|
||||||
|
return (&url.URL{
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Host: u.Host,
|
||||||
|
}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocalDev(u *url.URL) bool {
|
||||||
|
host := strings.ToLower(u.Hostname())
|
||||||
|
return u.Scheme == "http" &&
|
||||||
|
(host == "localhost" || host == "127.0.0.1")
|
||||||
}
|
}
|
||||||
|
|||||||
263
internal/handlers/cluster_runs.go
Normal file
263
internal/handlers/cluster_runs.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
|
"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"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListClusterRuns godoc
|
||||||
|
//
|
||||||
|
// @ID ListClusterRuns
|
||||||
|
// @Summary List cluster runs (org scoped)
|
||||||
|
// @Description Returns runs for a cluster within the organization in X-Org-ID.
|
||||||
|
// @Tags ClusterRuns
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param clusterID path string true "Cluster ID"
|
||||||
|
// @Success 200 {array} dto.ClusterRunResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "cluster not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /clusters/{clusterID}/runs [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListClusterRuns(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
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure cluster exists + org scoped
|
||||||
|
if err := db.Select("id").
|
||||||
|
Where("id = ? AND organization_id = ?", clusterID, orgID).
|
||||||
|
First(&models.Cluster{}).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.ClusterRun
|
||||||
|
if err := db.
|
||||||
|
Where("organization_id = ? AND cluster_id = ?", orgID, clusterID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]dto.ClusterRunResponse, 0, len(rows))
|
||||||
|
for _, cr := range rows {
|
||||||
|
out = append(out, clusterRunToDTO(cr))
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClusterRun godoc
|
||||||
|
//
|
||||||
|
// @ID GetClusterRun
|
||||||
|
// @Summary Get a cluster run (org scoped)
|
||||||
|
// @Description Returns a single run for a cluster within the organization in X-Org-ID.
|
||||||
|
// @Tags ClusterRuns
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param clusterID path string true "Cluster ID"
|
||||||
|
// @Param runID path string true "Run ID"
|
||||||
|
// @Success 200 {object} dto.ClusterRunResponse
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /clusters/{clusterID}/runs/{runID} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func GetClusterRun(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
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runID, err := uuid.Parse(chi.URLParam(r, "runID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_run_id", "invalid run id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.ClusterRun
|
||||||
|
if err := db.
|
||||||
|
Where("id = ? AND organization_id = ? AND cluster_id = ?", runID, orgID, clusterID).
|
||||||
|
First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "run not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, clusterRunToDTO(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunClusterAction godoc
|
||||||
|
//
|
||||||
|
// @ID RunClusterAction
|
||||||
|
// @Summary Run an admin-configured action on a cluster (org scoped)
|
||||||
|
// @Description Creates a ClusterRun record for the cluster/action. Execution is handled asynchronously by workers.
|
||||||
|
// @Tags ClusterRuns
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param clusterID path string true "Cluster ID"
|
||||||
|
// @Param actionID path string true "Action ID"
|
||||||
|
// @Success 201 {object} dto.ClusterRunResponse
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "cluster or action not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /clusters/{clusterID}/actions/{actionID}/runs [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func RunClusterAction(db *gorm.DB, jobs *bg.Jobs) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cluster must exist + org scoped
|
||||||
|
var cluster models.Cluster
|
||||||
|
if err := db.Select("id", "organization_id").
|
||||||
|
Where("id = ? AND organization_id = ?", clusterID, orgID).
|
||||||
|
First(&cluster).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// action is global/admin-configured (not org scoped)
|
||||||
|
var action models.Action
|
||||||
|
if err := db.Where("id = ?", actionID).First(&action).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "action_not_found", "action not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
run := models.ClusterRun{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
ClusterID: clusterID,
|
||||||
|
Action: action.MakeTarget, // this is what you actually execute
|
||||||
|
Status: models.ClusterRunStatusQueued,
|
||||||
|
Error: "",
|
||||||
|
FinishedAt: time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&run).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := bg.ClusterActionArgs{
|
||||||
|
OrgID: orgID,
|
||||||
|
ClusterID: clusterID,
|
||||||
|
Action: action.MakeTarget,
|
||||||
|
MakeTarget: action.MakeTarget,
|
||||||
|
}
|
||||||
|
// Enqueue with run.ID as the job ID so the worker can look it up.
|
||||||
|
_, enqueueErr := jobs.Enqueue(
|
||||||
|
r.Context(),
|
||||||
|
run.ID.String(),
|
||||||
|
"cluster_action",
|
||||||
|
args,
|
||||||
|
archer.WithMaxRetries(3),
|
||||||
|
)
|
||||||
|
|
||||||
|
if enqueueErr != nil {
|
||||||
|
_ = db.Model(&models.ClusterRun{}).
|
||||||
|
Where("id = ?", run.ID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": models.ClusterRunStatusFailed,
|
||||||
|
"error": "failed to enqueue job: " + enqueueErr.Error(),
|
||||||
|
"finished_at": time.Now().UTC(),
|
||||||
|
}).Error
|
||||||
|
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "job_error", "failed to enqueue cluster action")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, clusterRunToDTO(run))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clusterRunToDTO(cr models.ClusterRun) dto.ClusterRunResponse {
|
||||||
|
var finished *time.Time
|
||||||
|
if !cr.FinishedAt.IsZero() {
|
||||||
|
t := cr.FinishedAt
|
||||||
|
finished = &t
|
||||||
|
}
|
||||||
|
return dto.ClusterRunResponse{
|
||||||
|
ID: cr.ID,
|
||||||
|
OrganizationID: cr.OrganizationID,
|
||||||
|
ClusterID: cr.ClusterID,
|
||||||
|
Action: cr.Action,
|
||||||
|
Status: cr.Status,
|
||||||
|
Error: cr.Error,
|
||||||
|
CreatedAt: cr.CreatedAt,
|
||||||
|
UpdatedAt: cr.UpdatedAt,
|
||||||
|
FinishedAt: finished,
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -23,24 +23,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ListCredentials godoc
|
// ListCredentials godoc
|
||||||
// @ID ListCredentials
|
//
|
||||||
// @Summary List credentials (metadata only)
|
// @ID ListCredentials
|
||||||
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
// @Summary List credentials (metadata only)
|
||||||
// @Tags Credentials
|
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
||||||
// @Accept json
|
// @Tags Credentials
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||||
// @Param provider query string false "Filter by provider (e.g., aws)"
|
// @Param credential_provider query string false "Filter by provider (e.g., aws)"
|
||||||
// @Param kind query string false "Filter by kind (e.g., aws_access_key)"
|
// @Param kind query string false "Filter by kind (e.g., aws_access_key)"
|
||||||
// @Param scope_kind query string false "Filter by scope kind (provider/service/resource)"
|
// @Param scope_kind query string false "Filter by scope kind (credential_provider/service/resource)"
|
||||||
// @Success 200 {array} dto.CredentialOut
|
// @Success 200 {array} dto.CredentialOut
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 500 {string} string "internal server error"
|
// @Failure 500 {string} string "internal server error"
|
||||||
// @Router /credentials [get]
|
// @Router /credentials [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgSecretAuth
|
||||||
func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -49,7 +49,7 @@ func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
q := db.Where("organization_id = ?", orgID)
|
q := db.Where("organization_id = ?", orgID)
|
||||||
if v := r.URL.Query().Get("provider"); v != "" {
|
if v := r.URL.Query().Get("credential_provider"); v != "" {
|
||||||
q = q.Where("provider = ?", v)
|
q = q.Where("provider = ?", v)
|
||||||
}
|
}
|
||||||
if v := r.URL.Query().Get("kind"); v != "" {
|
if v := r.URL.Query().Get("kind"); v != "" {
|
||||||
@@ -73,21 +73,21 @@ func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCredential godoc
|
// GetCredential godoc
|
||||||
// @ID GetCredential
|
//
|
||||||
// @Summary Get credential by ID (metadata only)
|
// @ID GetCredential
|
||||||
// @Tags Credentials
|
// @Summary Get credential by ID (metadata only)
|
||||||
// @Accept json
|
// @Tags Credentials
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||||
// @Param id path string true "Credential ID (UUID)"
|
// @Param id path string true "Credential ID (UUID)"
|
||||||
// @Success 200 {object} dto.CredentialOut
|
// @Success 200 {object} dto.CredentialOut
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 500 {string} string "internal server error"
|
// @Failure 500 {string} string "internal server error"
|
||||||
// @Router /credentials/{id} [get]
|
// @Router /credentials/{id} [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgSecretAuth
|
||||||
func GetCredential(db *gorm.DB) http.HandlerFunc {
|
func GetCredential(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -117,21 +117,22 @@ func GetCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateCredential godoc
|
// CreateCredential godoc
|
||||||
// @ID CreateCredential
|
//
|
||||||
// @Summary Create a credential (encrypts secret)
|
// @ID CreateCredential
|
||||||
// @Tags Credentials
|
// @Summary Create a credential (encrypts secret)
|
||||||
// @Accept json
|
// @Tags Credentials
|
||||||
// @Produce json
|
// @Accept json
|
||||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
// @Produce json
|
||||||
// @Param body body dto.CreateCredentialRequest true "Credential payload"
|
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||||
// @Success 201 {object} dto.CredentialOut
|
// @Param body body dto.CreateCredentialRequest true "Credential payload"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Success 201 {object} dto.CredentialOut
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 500 {string} string "internal server error"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Router /credentials [post]
|
// @Failure 500 {string} string "internal server error"
|
||||||
// @Security BearerAuth
|
// @Router /credentials [post]
|
||||||
// @Security OrgKeyAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -153,7 +154,7 @@ func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
cred, err := SaveCredentialWithScope(
|
cred, err := SaveCredentialWithScope(
|
||||||
r.Context(), db, orgID,
|
r.Context(), db, orgID,
|
||||||
in.Provider, in.Kind, in.SchemaVersion,
|
in.CredentialProvider, in.Kind, in.SchemaVersion,
|
||||||
in.ScopeKind, in.ScopeVersion, json.RawMessage(in.Scope), json.RawMessage(in.Secret),
|
in.ScopeKind, in.ScopeVersion, json.RawMessage(in.Scope), json.RawMessage(in.Secret),
|
||||||
in.Name, in.AccountID, in.Region,
|
in.Name, in.AccountID, in.Region,
|
||||||
)
|
)
|
||||||
@@ -166,21 +167,22 @@ func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCredential godoc
|
// UpdateCredential godoc
|
||||||
// @ID UpdateCredential
|
//
|
||||||
// @Summary Update credential metadata and/or rotate secret
|
// @ID UpdateCredential
|
||||||
// @Tags Credentials
|
// @Summary Update credential metadata and/or rotate secret
|
||||||
// @Accept json
|
// @Tags Credentials
|
||||||
// @Produce json
|
// @Accept json
|
||||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
// @Produce json
|
||||||
// @Param id path string true "Credential ID (UUID)"
|
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||||
// @Param body body dto.UpdateCredentialRequest true "Fields to update"
|
// @Param id path string true "Credential ID (UUID)"
|
||||||
// @Success 200 {object} dto.CredentialOut
|
// @Param body body dto.UpdateCredentialRequest true "Fields to update"
|
||||||
// @Failure 403 {string} string "X-Org-ID required"
|
// @Success 200 {object} dto.CredentialOut
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 403 {string} string "X-Org-ID required"
|
||||||
// @Router /credentials/{id} [patch]
|
// @Failure 404 {string} string "not found"
|
||||||
// @Security BearerAuth
|
// @Router /credentials/{id} [patch]
|
||||||
// @Security OrgKeyAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -296,19 +298,19 @@ func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCredential godoc
|
// DeleteCredential godoc
|
||||||
// @ID DeleteCredential
|
//
|
||||||
// @Summary Delete credential
|
// @ID DeleteCredential
|
||||||
// @Tags Credentials
|
// @Summary Delete credential
|
||||||
// @Accept json
|
// @Tags Credentials
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||||
// @Param id path string true "Credential ID (UUID)"
|
// @Param id path string true "Credential ID (UUID)"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Router /credentials/{id} [delete]
|
// @Router /credentials/{id} [delete]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgSecretAuth
|
||||||
func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -335,20 +337,21 @@ func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RevealCredential godoc
|
// RevealCredential godoc
|
||||||
// @ID RevealCredential
|
//
|
||||||
// @Summary Reveal decrypted secret (one-time read)
|
// @ID RevealCredential
|
||||||
// @Tags Credentials
|
// @Summary Reveal decrypted secret (one-time read)
|
||||||
// @Accept json
|
// @Tags Credentials
|
||||||
// @Produce json
|
// @Accept json
|
||||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
// @Produce json
|
||||||
// @Param id path string true "Credential ID (UUID)"
|
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||||
// @Success 200 {object} map[string]any
|
// @Param id path string true "Credential ID (UUID)"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Success 200 {object} map[string]any
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Router /credentials/{id}/reveal [post]
|
// @Failure 404 {string} string "not found"
|
||||||
// @Security BearerAuth
|
// @Router /credentials/{id}/reveal [post]
|
||||||
// @Security OrgKeyAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
func RevealCredential(db *gorm.DB) http.HandlerFunc {
|
func RevealCredential(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -545,17 +548,17 @@ func SaveCredentialWithScope(
|
|||||||
// credOut converts model → response DTO
|
// credOut converts model → response DTO
|
||||||
func credOut(c *models.Credential) dto.CredentialOut {
|
func credOut(c *models.Credential) dto.CredentialOut {
|
||||||
return dto.CredentialOut{
|
return dto.CredentialOut{
|
||||||
ID: c.ID.String(),
|
ID: c.ID.String(),
|
||||||
Provider: c.Provider,
|
CredentialProvider: c.Provider,
|
||||||
Kind: c.Kind,
|
Kind: c.Kind,
|
||||||
SchemaVersion: c.SchemaVersion,
|
SchemaVersion: c.SchemaVersion,
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
ScopeKind: c.ScopeKind,
|
ScopeKind: c.ScopeKind,
|
||||||
ScopeVersion: c.ScopeVersion,
|
ScopeVersion: c.ScopeVersion,
|
||||||
Scope: dto.RawJSON(c.Scope),
|
Scope: dto.RawJSON(c.Scope),
|
||||||
AccountID: c.AccountID,
|
AccountID: c.AccountID,
|
||||||
Region: c.Region,
|
Region: c.Region,
|
||||||
CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339),
|
CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339),
|
UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ func mustSameOrgDomainWithCredential(db *gorm.DB, orgID uuid.UUID, credID uuid.U
|
|||||||
// @Summary List domains (org scoped)
|
// @Summary List domains (org scoped)
|
||||||
// @Description Returns domains for X-Org-ID. Filters: `domain_name`, `status`, `q` (contains).
|
// @Description Returns domains for X-Org-ID. Filters: `domain_name`, `status`, `q` (contains).
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param domain_name query string false "Exact domain name (lowercase, no trailing dot)"
|
// @Param domain_name query string false "Exact domain name (lowercase, no trailing dot)"
|
||||||
@@ -213,21 +212,20 @@ func ListDomains(db *gorm.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// GetDomain godoc
|
// GetDomain godoc
|
||||||
//
|
//
|
||||||
// @ID GetDomain
|
// @ID GetDomain
|
||||||
// @Summary Get a domain (org scoped)
|
// @Summary Get a domain (org scoped)
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Produce json
|
||||||
// @Produce json
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param id path string true "Domain ID (UUID)"
|
||||||
// @Param id path string true "Domain ID (UUID)"
|
// @Success 200 {object} dto.DomainResponse
|
||||||
// @Success 200 {object} dto.DomainResponse
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Router /dns/domains/{id} [get]
|
||||||
// @Router /dns/domains/{id} [get]
|
// @Security BearerAuth
|
||||||
// @Security BearerAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgSecretAuth
|
||||||
// @Security OrgSecretAuth
|
|
||||||
func GetDomain(db *gorm.DB) http.HandlerFunc {
|
func GetDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -261,7 +259,7 @@ func GetDomain(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param body body dto.CreateDomainRequest true "Domain payload"
|
// @Param body body dto.CreateDomainRequest true "Domain payload"
|
||||||
// @Success 201 {object} dto.DomainResponse
|
// @Success 201 {object} dto.DomainResponse
|
||||||
// @Failure 400 {string} string "validation error"
|
// @Failure 400 {string} string "validation error"
|
||||||
@@ -312,22 +310,22 @@ func CreateDomain(db *gorm.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// UpdateDomain godoc
|
// UpdateDomain godoc
|
||||||
//
|
//
|
||||||
// @ID UpdateDomain
|
// @ID UpdateDomain
|
||||||
// @Summary Update a domain (org scoped)
|
// @Summary Update a domain (org scoped)
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Domain ID (UUID)"
|
// @Param id path string true "Domain ID (UUID)"
|
||||||
// @Param body body dto.UpdateDomainRequest true "Fields to update"
|
// @Param body body dto.UpdateDomainRequest true "Fields to update"
|
||||||
// @Success 200 {object} dto.DomainResponse
|
// @Success 200 {object} dto.DomainResponse
|
||||||
// @Failure 400 {string} string "validation error"
|
// @Failure 400 {string} string "validation error"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Router /dns/domains/{id} [patch]
|
// @Router /dns/domains/{id} [patch]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgSecretAuth
|
||||||
func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -390,20 +388,19 @@ func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// DeleteDomain godoc
|
// DeleteDomain godoc
|
||||||
//
|
//
|
||||||
// @ID DeleteDomain
|
// @ID DeleteDomain
|
||||||
// @Summary Delete a domain
|
// @Summary Delete a domain
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Produce json
|
||||||
// @Produce json
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param id path string true "Domain ID (UUID)"
|
||||||
// @Param id path string true "Domain ID (UUID)"
|
// @Success 204
|
||||||
// @Success 204
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Router /dns/domains/{id} [delete]
|
||||||
// @Router /dns/domains/{id} [delete]
|
// @Security BearerAuth
|
||||||
// @Security BearerAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgSecretAuth
|
||||||
// @Security OrgSecretAuth
|
|
||||||
func DeleteDomain(db *gorm.DB) http.HandlerFunc {
|
func DeleteDomain(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -437,13 +434,12 @@ func DeleteDomain(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary List record sets for a domain
|
// @Summary List record sets for a domain
|
||||||
// @Description Filters: `name`, `type`, `status`.
|
// @Description Filters: `name`, `type`, `status`.
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param domain_id path string true "Domain ID (UUID)"
|
// @Param domain_id path string true "Domain ID (UUID)"
|
||||||
// @Param name query string false "Exact relative name or FQDN (server normalizes)"
|
// @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 type query string false "RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)"
|
||||||
// @Param status query string false "pending|provisioning|ready|failed"
|
// @Param status query string false "pending|provisioning|ready|failed"
|
||||||
// @Success 200 {array} dto.RecordSetResponse
|
// @Success 200 {array} dto.RecordSetResponse
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "domain not found"
|
// @Failure 404 {string} string "domain not found"
|
||||||
@@ -507,24 +503,69 @@ func ListRecordSets(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GetRecordSet godoc
|
||||||
|
//
|
||||||
|
// @ID GetRecordSet
|
||||||
|
// @Summary Get a record set (org scoped)
|
||||||
|
// @Tags DNS
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Record Set ID (UUID)"
|
||||||
|
// @Success 200 {object} dto.RecordSetResponse
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /dns/records/{id} [get]
|
||||||
|
func GetRecordSet(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
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, recordOut(&row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRecordSet godoc
|
// CreateRecordSet godoc
|
||||||
//
|
//
|
||||||
// @ID CreateRecordSet
|
// @ID CreateRecordSet
|
||||||
// @Summary Create a record set (pending; Archer will UPSERT to Route 53)
|
// @Summary Create a record set (pending; Archer will UPSERT to Route 53)
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param domain_id path string true "Domain ID (UUID)"
|
// @Param domain_id path string true "Domain ID (UUID)"
|
||||||
// @Param body body dto.CreateRecordSetRequest true "Record set payload"
|
// @Param body body dto.CreateRecordSetRequest true "Record set payload"
|
||||||
// @Success 201 {object} dto.RecordSetResponse
|
// @Success 201 {object} dto.RecordSetResponse
|
||||||
// @Failure 400 {string} string "validation error"
|
// @Failure 400 {string} string "validation error"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "domain not found"
|
// @Failure 404 {string} string "domain not found"
|
||||||
// @Router /dns/domains/{domain_id}/records [post]
|
// @Router /dns/domains/{domain_id}/records [post]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgSecretAuth
|
||||||
func CreateRecordSet(db *gorm.DB) http.HandlerFunc {
|
func CreateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -610,22 +651,22 @@ func CreateRecordSet(db *gorm.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// UpdateRecordSet godoc
|
// UpdateRecordSet godoc
|
||||||
//
|
//
|
||||||
// @ID UpdateRecordSet
|
// @ID UpdateRecordSet
|
||||||
// @Summary Update a record set (flips to pending for reconciliation)
|
// @Summary Update a record set (flips to pending for reconciliation)
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Record Set ID (UUID)"
|
// @Param id path string true "Record Set ID (UUID)"
|
||||||
// @Param body body dto.UpdateRecordSetRequest true "Fields to update"
|
// @Param body body dto.UpdateRecordSetRequest true "Fields to update"
|
||||||
// @Success 200 {object} dto.RecordSetResponse
|
// @Success 200 {object} dto.RecordSetResponse
|
||||||
// @Failure 400 {string} string "validation error"
|
// @Failure 400 {string} string "validation error"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Router /dns/records/{id} [patch]
|
// @Router /dns/records/{id} [patch]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgSecretAuth
|
// @Security OrgSecretAuth
|
||||||
func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
@@ -720,20 +761,19 @@ func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// DeleteRecordSet godoc
|
// DeleteRecordSet godoc
|
||||||
//
|
//
|
||||||
// @ID DeleteRecordSet
|
// @ID DeleteRecordSet
|
||||||
// @Summary Delete a record set (API removes row; worker can optionally handle external deletion policy)
|
// @Summary Delete a record set (API removes row; worker can optionally handle external deletion policy)
|
||||||
// @Tags DNS
|
// @Tags DNS
|
||||||
// @Accept json
|
// @Produce json
|
||||||
// @Produce json
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param id path string true "Record Set ID (UUID)"
|
||||||
// @Param id path string true "Record Set ID (UUID)"
|
// @Success 204
|
||||||
// @Success 204
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Router /dns/records/{id} [delete]
|
||||||
// @Router /dns/records/{id} [delete]
|
// @Security BearerAuth
|
||||||
// @Security BearerAuth
|
// @Security OrgKeyAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgSecretAuth
|
||||||
// @Security OrgSecretAuth
|
|
||||||
func DeleteRecordSet(db *gorm.DB) http.HandlerFunc {
|
func DeleteRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
|||||||
28
internal/handlers/dto/actions.go
Normal file
28
internal/handlers/dto/actions.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
MakeTarget string `json:"make_target"`
|
||||||
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateActionRequest struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
MakeTarget string `json:"make_target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateActionRequest struct {
|
||||||
|
Label *string `json:"label,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
MakeTarget *string `json:"make_target,omitempty"`
|
||||||
|
}
|
||||||
19
internal/handlers/dto/cluster_runs.go
Normal file
19
internal/handlers/dto/cluster_runs.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterRunResponse struct {
|
||||||
|
ID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||||
|
ClusterID uuid.UUID `json:"cluster_id" format:"uuid"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||||
|
FinishedAt *time.Time `json:"finished_at,omitempty" format:"date-time"`
|
||||||
|
}
|
||||||
@@ -7,28 +7,66 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClusterResponse struct {
|
type ClusterResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Provider string `json:"provider"`
|
CaptainDomain *DomainResponse `json:"captain_domain,omitempty"`
|
||||||
Region string `json:"region"`
|
ControlPlaneRecordSet *RecordSetResponse `json:"control_plane_record_set,omitempty"`
|
||||||
Status string `json:"status"`
|
ControlPlaneFQDN *string `json:"control_plane_fqdn,omitempty"`
|
||||||
CaptainDomain string `json:"captain_domain"`
|
AppsLoadBalancer *LoadBalancerResponse `json:"apps_load_balancer,omitempty"`
|
||||||
ClusterLoadBalancer string `json:"cluster_load_balancer"`
|
GlueOpsLoadBalancer *LoadBalancerResponse `json:"glueops_load_balancer,omitempty"`
|
||||||
RandomToken string `json:"random_token"`
|
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
||||||
CertificateKey string `json:"certificate_key"`
|
Provider string `json:"cluster_provider"`
|
||||||
ControlLoadBalancer string `json:"control_load_balancer"`
|
Region string `json:"region"`
|
||||||
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
Status string `json:"status"`
|
||||||
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
LastError string `json:"last_error"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
RandomToken string `json:"random_token"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CertificateKey string `json:"certificate_key"`
|
||||||
|
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
||||||
|
DockerImage string `json:"docker_image"`
|
||||||
|
DockerTag string `json:"docker_tag"`
|
||||||
|
Kubeconfig *string `json:"kubeconfig,omitempty"`
|
||||||
|
OrgKey *string `json:"org_key,omitempty"`
|
||||||
|
OrgSecret *string `json:"org_secret,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateClusterRequest struct {
|
type CreateClusterRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Provider string `json:"provider"`
|
ClusterProvider string `json:"cluster_provider"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Status string `json:"status"`
|
DockerImage string `json:"docker_image"`
|
||||||
CaptainDomain string `json:"captain_domain"`
|
DockerTag string `json:"docker_tag"`
|
||||||
ClusterLoadBalancer *string `json:"cluster_load_balancer"`
|
}
|
||||||
ControlLoadBalancer *string `json:"control_load_balancer"`
|
|
||||||
|
type UpdateClusterRequest struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
ClusterProvider *string `json:"cluster_provider,omitempty"`
|
||||||
|
Region *string `json:"region,omitempty"`
|
||||||
|
DockerImage *string `json:"docker_image,omitempty"`
|
||||||
|
DockerTag *string `json:"docker_tag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachCaptainDomainRequest struct {
|
||||||
|
DomainID uuid.UUID `json:"domain_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachRecordSetRequest struct {
|
||||||
|
RecordSetID uuid.UUID `json:"record_set_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachLoadBalancerRequest struct {
|
||||||
|
LoadBalancerID uuid.UUID `json:"load_balancer_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachBastionRequest struct {
|
||||||
|
ServerID uuid.UUID `json:"server_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetKubeconfigRequest struct {
|
||||||
|
Kubeconfig string `json:"kubeconfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachNodePoolRequest struct {
|
||||||
|
NodePoolID uuid.UUID `json:"node_pool_id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,16 +97,16 @@ var ScopeRegistry = map[string]map[string]map[int]ScopeDef{
|
|||||||
|
|
||||||
// CreateCredentialRequest represents the POST /credentials payload
|
// CreateCredentialRequest represents the POST /credentials payload
|
||||||
type CreateCredentialRequest struct {
|
type CreateCredentialRequest struct {
|
||||||
Provider string `json:"provider" validate:"required,oneof=aws cloudflare hetzner digitalocean generic"`
|
CredentialProvider string `json:"credential_provider" validate:"required,oneof=aws cloudflare hetzner digitalocean generic"`
|
||||||
Kind string `json:"kind" validate:"required"` // aws_access_key, api_token, basic_auth, oauth2
|
Kind string `json:"kind" validate:"required"` // aws_access_key, api_token, basic_auth, oauth2
|
||||||
SchemaVersion int `json:"schema_version" validate:"required,gte=1"` // secret schema version
|
SchemaVersion int `json:"schema_version" validate:"required,gte=1"` // secret schema version
|
||||||
Name string `json:"name" validate:"omitempty,max=100"` // human label
|
Name string `json:"name" validate:"omitempty,max=100"` // human label
|
||||||
ScopeKind string `json:"scope_kind" validate:"required,oneof=provider service resource"`
|
ScopeKind string `json:"scope_kind" validate:"required,oneof=credential_provider service resource"`
|
||||||
ScopeVersion int `json:"scope_version" validate:"required,gte=1"` // scope schema version
|
ScopeVersion int `json:"scope_version" validate:"required,gte=1"` // scope schema version
|
||||||
Scope RawJSON `json:"scope" validate:"required" swaggertype:"object"` // {"service":"route53"} or {"arn":"..."}
|
Scope RawJSON `json:"scope" validate:"required" swaggertype:"object"` // {"service":"route53"} or {"arn":"..."}
|
||||||
AccountID string `json:"account_id,omitempty" validate:"omitempty,max=32"`
|
AccountID string `json:"account_id,omitempty" validate:"omitempty,max=32"`
|
||||||
Region string `json:"region,omitempty" validate:"omitempty,max=32"`
|
Region string `json:"region,omitempty" validate:"omitempty,max=32"`
|
||||||
Secret RawJSON `json:"secret" validate:"required" swaggertype:"object"` // encrypted later
|
Secret RawJSON `json:"secret" validate:"required" swaggertype:"object"` // encrypted later
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCredentialRequest represents PATCH /credentials/{id}
|
// UpdateCredentialRequest represents PATCH /credentials/{id}
|
||||||
@@ -123,16 +123,16 @@ type UpdateCredentialRequest struct {
|
|||||||
|
|
||||||
// CredentialOut is what we return (no secrets)
|
// CredentialOut is what we return (no secrets)
|
||||||
type CredentialOut struct {
|
type CredentialOut struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Provider string `json:"provider"`
|
CredentialProvider string `json:"credential_provider"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
SchemaVersion int `json:"schema_version"`
|
SchemaVersion int `json:"schema_version"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ScopeKind string `json:"scope_kind"`
|
ScopeKind string `json:"scope_kind"`
|
||||||
ScopeVersion int `json:"scope_version"`
|
ScopeVersion int `json:"scope_version"`
|
||||||
Scope RawJSON `json:"scope" swaggertype:"object"`
|
Scope RawJSON `json:"scope" swaggertype:"object"`
|
||||||
AccountID string `json:"account_id,omitempty"`
|
AccountID string `json:"account_id,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
32
internal/handlers/dto/load_balancers.go
Normal file
32
internal/handlers/dto/load_balancers.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoadBalancerResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
PublicIPAddress string `json:"public_ip_address"`
|
||||||
|
PrivateIPAddress string `json:"private_ip_address"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLoadBalancerRequest struct {
|
||||||
|
Name string `json:"name" example:"glueops"`
|
||||||
|
Kind string `json:"kind" example:"public" enums:"glueops,public"`
|
||||||
|
PublicIPAddress string `json:"public_ip_address" example:"8.8.8.8"`
|
||||||
|
PrivateIPAddress string `json:"private_ip_address" example:"192.168.0.2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLoadBalancerRequest struct {
|
||||||
|
Name *string `json:"name" example:"glue"`
|
||||||
|
Kind *string `json:"kind" example:"public" enums:"glueops,public"`
|
||||||
|
PublicIPAddress *string `json:"public_ip_address" example:"8.8.8.8"`
|
||||||
|
PrivateIPAddress *string `json:"private_ip_address" example:"192.168.0.2"`
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ type HealthStatus struct {
|
|||||||
// @Description Returns 200 OK when the service is up
|
// @Description Returns 200 OK when the service is up
|
||||||
// @Tags Health
|
// @Tags Health
|
||||||
// @ID HealthCheck // operationId
|
// @ID HealthCheck // operationId
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} HealthStatus
|
// @Success 200 {object} HealthStatus
|
||||||
// @Router /healthz [get]
|
// @Router /healthz [get]
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
// @Summary List Archer jobs (admin)
|
// @Summary List Archer jobs (admin)
|
||||||
// @Description Paginated background jobs with optional filters. Search `q` may match id, type, error, payload (implementation-dependent).
|
// @Description Paginated background jobs with optional filters. Search `q` may match id, type, error, payload (implementation-dependent).
|
||||||
// @Tags ArcherAdmin
|
// @Tags ArcherAdmin
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param status query string false "Filter by status" Enums(queued,running,succeeded,failed,canceled,retrying,scheduled)
|
// @Param status query string false "Filter by status" Enums(queued,running,succeeded,failed,canceled,retrying,scheduled)
|
||||||
// @Param queue query string false "Filter by queue name / worker name"
|
// @Param queue query string false "Filter by queue name / worker name"
|
||||||
@@ -283,7 +282,6 @@ func AdminCancelArcherJob(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary List Archer queues (admin)
|
// @Summary List Archer queues (admin)
|
||||||
// @Description Summary metrics per queue (pending, running, failed, scheduled).
|
// @Description Summary metrics per queue (pending, running, failed, scheduled).
|
||||||
// @Tags ArcherAdmin
|
// @Tags ArcherAdmin
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} dto.QueueInfo
|
// @Success 200 {array} dto.QueueInfo
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
// @Summary List node labels (org scoped)
|
// @Summary List node labels (org scoped)
|
||||||
// @Description Returns node labels for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node groups.
|
// @Description Returns node labels for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node groups.
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param key query string false "Exact key"
|
// @Param key query string false "Exact key"
|
||||||
@@ -74,7 +73,6 @@ func ListLabels(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Get label by ID (org scoped)
|
// @Summary Get label by ID (org scoped)
|
||||||
// @Description Returns one label.
|
// @Description Returns one label.
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Label ID (UUID)"
|
// @Param id path string true "Label ID (UUID)"
|
||||||
@@ -253,11 +251,10 @@ func UpdateLabel(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Delete label (org scoped)
|
// @Summary Delete label (org scoped)
|
||||||
// @Description Permanently deletes the label.
|
// @Description Permanently deletes the label.
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Label ID (UUID)"
|
// @Param id path string true "Label ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
|
|||||||
286
internal/handlers/load_balancers.go
Normal file
286
internal/handlers/load_balancers.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListLoadBalancers godoc
|
||||||
|
//
|
||||||
|
// @ID ListLoadBalancers
|
||||||
|
// @Summary List load balancers (org scoped)
|
||||||
|
// @Description Returns load balancers for the organization in X-Org-ID.
|
||||||
|
// @Tags LoadBalancers
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Success 200 {array} dto.LoadBalancerResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "failed to list clusters"
|
||||||
|
// @Router /load-balancers [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListLoadBalancers(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 rows []models.LoadBalancer
|
||||||
|
if err := db.Where("organization_id = ?", orgID).Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]dto.LoadBalancerResponse, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, loadBalancerOut(&row))
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoadBalancer godoc
|
||||||
|
//
|
||||||
|
// @ID GetLoadBalancers
|
||||||
|
// @Summary Get a load balancer (org scoped)
|
||||||
|
// @Description Returns load balancer for the organization in X-Org-ID.
|
||||||
|
// @Tags LoadBalancers
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "LoadBalancer ID (UUID)"
|
||||||
|
// @Success 200 {array} dto.LoadBalancerResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "failed to list clusters"
|
||||||
|
// @Router /load-balancers/{id} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func GetLoadBalancer(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.LoadBalancer
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "load balancer not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := loadBalancerOut(&row)
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLoadBalancer godoc
|
||||||
|
//
|
||||||
|
// @ID CreateLoadBalancer
|
||||||
|
// @Summary Create a load balancer
|
||||||
|
// @Tags LoadBalancers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param body body dto.CreateLoadBalancerRequest true "Record set payload"
|
||||||
|
// @Success 201 {object} dto.LoadBalancerResponse
|
||||||
|
// @Failure 400 {string} string "validation error"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "domain not found"
|
||||||
|
// @Router /load-balancers [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func CreateLoadBalancer(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.CreateLoadBalancerRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(in.Kind) != "glueops" && strings.ToLower(in.Kind) != "public" {
|
||||||
|
fmt.Println(in.Kind)
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_kind", "invalid kind only 'glueops' or 'public'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
row := &models.LoadBalancer{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Name: in.Name,
|
||||||
|
Kind: strings.ToLower(in.Kind),
|
||||||
|
PublicIPAddress: in.PublicIPAddress,
|
||||||
|
PrivateIPAddress: in.PrivateIPAddress,
|
||||||
|
}
|
||||||
|
if err := db.Create(row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, loadBalancerOut(row))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLoadBalancer godoc
|
||||||
|
//
|
||||||
|
// @ID UpdateLoadBalancer
|
||||||
|
// @Summary Update a load balancer (org scoped)
|
||||||
|
// @Tags LoadBalancers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Load Balancer ID (UUID)"
|
||||||
|
// @Param body body dto.UpdateLoadBalancerRequest true "Fields to update"
|
||||||
|
// @Success 200 {object} dto.LoadBalancerResponse
|
||||||
|
// @Failure 400 {string} string "validation error"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /load-balancers/{id} [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func UpdateLoadBalancer(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
|
||||||
|
}
|
||||||
|
|
||||||
|
row := &models.LoadBalancer{}
|
||||||
|
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", "load balancer not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in dto.UpdateLoadBalancerRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if in.Name != nil {
|
||||||
|
row.Name = *in.Name
|
||||||
|
}
|
||||||
|
if in.Kind != nil {
|
||||||
|
fmt.Println(*in.Kind)
|
||||||
|
if strings.ToLower(*in.Kind) != "glueops" && strings.ToLower(*in.Kind) != "public" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_kind", "invalid kind only 'glueops' or 'public'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.Kind = strings.ToLower(*in.Kind)
|
||||||
|
}
|
||||||
|
if in.PublicIPAddress != nil {
|
||||||
|
row.PublicIPAddress = *in.PublicIPAddress
|
||||||
|
}
|
||||||
|
if in.PrivateIPAddress != nil {
|
||||||
|
row.PrivateIPAddress = *in.PrivateIPAddress
|
||||||
|
}
|
||||||
|
if err := db.Save(row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, loadBalancerOut(row))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLoadBalancer godoc
|
||||||
|
//
|
||||||
|
// @ID DeleteLoadBalancer
|
||||||
|
// @Summary Delete a load balancer
|
||||||
|
// @Tags LoadBalancers
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Load Balancer ID (UUID)"
|
||||||
|
// @Success 204
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Router /load-balancers/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DeleteLoadBalancer(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
|
||||||
|
}
|
||||||
|
|
||||||
|
row := &models.LoadBalancer{}
|
||||||
|
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", "load balancer not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Delete(row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Out mappers ----------
|
||||||
|
|
||||||
|
func loadBalancerOut(m *models.LoadBalancer) dto.LoadBalancerResponse {
|
||||||
|
return dto.LoadBalancerResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
OrganizationID: m.OrganizationID,
|
||||||
|
Name: m.Name,
|
||||||
|
Kind: m.Kind,
|
||||||
|
PublicIPAddress: m.PublicIPAddress,
|
||||||
|
PrivateIPAddress: m.PrivateIPAddress,
|
||||||
|
CreatedAt: m.CreatedAt.UTC(),
|
||||||
|
UpdatedAt: m.UpdatedAt.UTC(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,14 +25,13 @@ import (
|
|||||||
// @Summary List node pools (org scoped)
|
// @Summary List node pools (org scoped)
|
||||||
// @Description Returns node pools for the organization in X-Org-ID.
|
// @Description Returns node pools for the organization in X-Org-ID.
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param q query string false "Name contains (case-insensitive)"
|
// @Param q query string false "Name contains (case-insensitive)"
|
||||||
// @Success 200 {array} dto.NodePoolResponse
|
// @Success 200 {array} dto.NodePoolResponse
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 500 {string} string "failed to list node pools"
|
// @Failure 500 {string} string "failed to list node pools"
|
||||||
// @Router /node-pools [get]
|
// @Router /node-pools [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -145,16 +144,15 @@ func ListNodePools(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Get node pool by ID (org scoped)
|
// @Summary Get node pool by ID (org scoped)
|
||||||
// @Description Returns one node pool. Add `include=servers` to include servers.
|
// @Description Returns one node pool. Add `include=servers` to include servers.
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Success 200 {object} dto.NodePoolResponse
|
// @Success 200 {object} dto.NodePoolResponse
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "fetch failed"
|
// @Failure 500 {string} string "fetch failed"
|
||||||
// @Router /node-pools/{id} [get]
|
// @Router /node-pools/{id} [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -194,13 +192,13 @@ func GetNodePool(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param body body dto.CreateNodePoolRequest true "NodePool payload"
|
// @Param body body dto.CreateNodePoolRequest true "NodePool payload"
|
||||||
// @Success 201 {object} dto.NodePoolResponse
|
// @Success 201 {object} dto.NodePoolResponse
|
||||||
// @Failure 400 {string} string "invalid json / missing fields / invalid server_ids"
|
// @Failure 400 {string} string "invalid json / missing fields / invalid server_ids"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 500 {string} string "create failed"
|
// @Failure 500 {string} string "create failed"
|
||||||
// @Router /node-pools [post]
|
// @Router /node-pools [post]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -257,15 +255,15 @@ func CreateNodePool(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param body body dto.UpdateNodePoolRequest true "Fields to update"
|
// @Param body body dto.UpdateNodePoolRequest true "Fields to update"
|
||||||
// @Success 200 {object} dto.NodePoolResponse
|
// @Success 200 {object} dto.NodePoolResponse
|
||||||
// @Failure 400 {string} string "invalid id / invalid json"
|
// @Failure 400 {string} string "invalid id / invalid json"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "update failed"
|
// @Failure 500 {string} string "update failed"
|
||||||
// @Router /node-pools/{id} [patch]
|
// @Router /node-pools/{id} [patch]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -327,15 +325,14 @@ func UpdateNodePool(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Delete node pool (org scoped)
|
// @Summary Delete node pool (org scoped)
|
||||||
// @Description Permanently deletes the node pool.
|
// @Description Permanently deletes the node pool.
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 500 {string} string "delete failed"
|
// @Failure 500 {string} string "delete failed"
|
||||||
// @Router /node-pools/{id} [delete]
|
// @Router /node-pools/{id} [delete]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -369,16 +366,15 @@ func DeleteNodePool(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID ListNodePoolServers
|
// @ID ListNodePoolServers
|
||||||
// @Summary List servers attached to a node pool (org scoped)
|
// @Summary List servers attached to a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Success 200 {array} dto.ServerResponse
|
// @Success 200 {array} dto.ServerResponse
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "fetch failed"
|
// @Failure 500 {string} string "fetch failed"
|
||||||
// @Router /node-pools/{id}/servers [get]
|
// @Router /node-pools/{id}/servers [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -434,15 +430,15 @@ func ListNodePoolServers(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param body body dto.AttachServersRequest true "Server IDs to attach"
|
// @Param body body dto.AttachServersRequest true "Server IDs to attach"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "attach failed"
|
// @Failure 500 {string} string "attach failed"
|
||||||
// @Router /node-pools/{id}/servers [post]
|
// @Router /node-pools/{id}/servers [post]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -521,17 +517,16 @@ func AttachNodePoolServers(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID DetachNodePoolServer
|
// @ID DetachNodePoolServer
|
||||||
// @Summary Detach one server from a node pool (org scoped)
|
// @Summary Detach one server from a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param serverId path string true "Server ID (UUID)"
|
// @Param serverId path string true "Server ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "detach failed"
|
// @Failure 500 {string} string "detach failed"
|
||||||
// @Router /node-pools/{id}/servers/{serverId} [delete]
|
// @Router /node-pools/{id}/servers/{serverId} [delete]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -588,16 +583,15 @@ func DetachNodePoolServer(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID ListNodePoolTaints
|
// @ID ListNodePoolTaints
|
||||||
// @Summary List taints attached to a node pool (org scoped)
|
// @Summary List taints attached to a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Success 200 {array} dto.TaintResponse
|
// @Success 200 {array} dto.TaintResponse
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "fetch failed"
|
// @Failure 500 {string} string "fetch failed"
|
||||||
// @Router /node-pools/{id}/taints [get]
|
// @Router /node-pools/{id}/taints [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -646,15 +640,15 @@ func ListNodePoolTaints(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param body body dto.AttachTaintsRequest true "Taint IDs to attach"
|
// @Param body body dto.AttachTaintsRequest true "Taint IDs to attach"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id / invalid taint_ids"
|
// @Failure 400 {string} string "invalid id / invalid taint_ids"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "attach failed"
|
// @Failure 500 {string} string "attach failed"
|
||||||
// @Router /node-pools/{id}/taints [post]
|
// @Router /node-pools/{id}/taints [post]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -730,17 +724,16 @@ func AttachNodePoolTaints(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID DetachNodePoolTaint
|
// @ID DetachNodePoolTaint
|
||||||
// @Summary Detach one taint from a node pool (org scoped)
|
// @Summary Detach one taint from a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param taintId path string true "Taint ID (UUID)"
|
// @Param taintId path string true "Taint ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "detach failed"
|
// @Failure 500 {string} string "detach failed"
|
||||||
// @Router /node-pools/{id}/taints/{taintId} [delete]
|
// @Router /node-pools/{id}/taints/{taintId} [delete]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -798,16 +791,15 @@ func DetachNodePoolTaint(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID ListNodePoolLabels
|
// @ID ListNodePoolLabels
|
||||||
// @Summary List labels attached to a node pool (org scoped)
|
// @Summary List labels attached to a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Label Pool ID (UUID)"
|
// @Param id path string true "Label Pool ID (UUID)"
|
||||||
// @Success 200 {array} dto.LabelResponse
|
// @Success 200 {array} dto.LabelResponse
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "fetch failed"
|
// @Failure 500 {string} string "fetch failed"
|
||||||
// @Router /node-pools/{id}/labels [get]
|
// @Router /node-pools/{id}/labels [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -836,16 +828,16 @@ func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.LabelResponse, 0, len(np.Taints))
|
out := make([]dto.LabelResponse, 0, len(np.Taints))
|
||||||
for _, taint := range np.Taints {
|
for _, label := range np.Labels {
|
||||||
out = append(out, dto.LabelResponse{
|
out = append(out, dto.LabelResponse{
|
||||||
AuditFields: common.AuditFields{
|
AuditFields: common.AuditFields{
|
||||||
ID: taint.ID,
|
ID: label.ID,
|
||||||
OrganizationID: taint.OrganizationID,
|
OrganizationID: label.OrganizationID,
|
||||||
CreatedAt: taint.CreatedAt,
|
CreatedAt: label.CreatedAt,
|
||||||
UpdatedAt: taint.UpdatedAt,
|
UpdatedAt: label.UpdatedAt,
|
||||||
},
|
},
|
||||||
Key: taint.Key,
|
Key: label.Key,
|
||||||
Value: taint.Value,
|
Value: label.Value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
utils.WriteJSON(w, http.StatusOK, out)
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
@@ -859,15 +851,15 @@ func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param body body dto.AttachLabelsRequest true "Label IDs to attach"
|
// @Param body body dto.AttachLabelsRequest true "Label IDs to attach"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "attach failed"
|
// @Failure 500 {string} string "attach failed"
|
||||||
// @Router /node-pools/{id}/labels [post]
|
// @Router /node-pools/{id}/labels [post]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -940,17 +932,16 @@ func AttachNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID DetachNodePoolLabel
|
// @ID DetachNodePoolLabel
|
||||||
// @Summary Detach one label from a node pool (org scoped)
|
// @Summary Detach one label from a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param labelId path string true "Label ID (UUID)"
|
// @Param labelId path string true "Label ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "detach failed"
|
// @Failure 500 {string} string "detach failed"
|
||||||
// @Router /node-pools/{id}/labels/{labelId} [delete]
|
// @Router /node-pools/{id}/labels/{labelId} [delete]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -1008,16 +999,15 @@ func DetachNodePoolLabel(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID ListNodePoolAnnotations
|
// @ID ListNodePoolAnnotations
|
||||||
// @Summary List annotations attached to a node pool (org scoped)
|
// @Summary List annotations attached to a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Success 200 {array} dto.AnnotationResponse
|
// @Success 200 {array} dto.AnnotationResponse
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "fetch failed"
|
// @Failure 500 {string} string "fetch failed"
|
||||||
// @Router /node-pools/{id}/annotations [get]
|
// @Router /node-pools/{id}/annotations [get]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -1069,15 +1059,15 @@ func ListNodePoolAnnotations(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Group ID (UUID)"
|
// @Param id path string true "Node Group ID (UUID)"
|
||||||
// @Param body body dto.AttachAnnotationsRequest true "Annotation IDs to attach"
|
// @Param body body dto.AttachAnnotationsRequest true "Annotation IDs to attach"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "attach failed"
|
// @Failure 500 {string} string "attach failed"
|
||||||
// @Router /node-pools/{id}/annotations [post]
|
// @Router /node-pools/{id}/annotations [post]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
@@ -1151,17 +1141,16 @@ func AttachNodePoolAnnotations(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID DetachNodePoolAnnotation
|
// @ID DetachNodePoolAnnotation
|
||||||
// @Summary Detach one annotation from a node pool (org scoped)
|
// @Summary Detach one annotation from a node pool (org scoped)
|
||||||
// @Tags NodePools
|
// @Tags NodePools
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Pool ID (UUID)"
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
// @Param annotationId path string true "Annotation ID (UUID)"
|
// @Param annotationId path string true "Annotation ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 {string} string "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
// @Failure 404 {string} string "not found"
|
// @Failure 404 {string} string "not found"
|
||||||
// @Failure 500 {string} string "detach failed"
|
// @Failure 500 {string} string "detach failed"
|
||||||
// @Router /node-pools/{id}/annotations/{annotationId} [delete]
|
// @Router /node-pools/{id}/annotations/{annotationId} [delete]
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Security OrgKeyAuth
|
// @Security OrgKeyAuth
|
||||||
|
|||||||
381
internal/handlers/node_pools_test.go
Normal file
381
internal/handlers/node_pools_test.go
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/common"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/testutil/pgtest"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
code := m.Run()
|
||||||
|
pgtest.Stop()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUUIDs_Success(t *testing.T) {
|
||||||
|
u1 := uuid.New()
|
||||||
|
u2 := uuid.New()
|
||||||
|
|
||||||
|
got, err := parseUUIDs([]string{u1.String(), u2.String()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseUUIDs returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 UUIDs, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0] != u1 || got[1] != u2 {
|
||||||
|
t.Fatalf("unexpected UUIDs: got=%v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUUIDs_Invalid(t *testing.T) {
|
||||||
|
_, err := parseUUIDs([]string{"not-a-uuid"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for invalid UUID, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ensureServersBelongToOrg ---
|
||||||
|
|
||||||
|
func TestEnsureServersBelongToOrg_AllBelong(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
org := models.Organization{Name: "org-a"}
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
t.Fatalf("create org: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKey := createTestSshKey(t, db, org.ID, "org-a-key")
|
||||||
|
|
||||||
|
s1 := models.Server{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
Hostname: "srv-1",
|
||||||
|
SSHUser: "ubuntu",
|
||||||
|
SshKeyID: sshKey.ID,
|
||||||
|
Role: "worker",
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
s2 := models.Server{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
Hostname: "srv-2",
|
||||||
|
SSHUser: "ubuntu",
|
||||||
|
SshKeyID: sshKey.ID,
|
||||||
|
Role: "worker",
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&s1).Error; err != nil {
|
||||||
|
t.Fatalf("create server 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&s2).Error; err != nil {
|
||||||
|
t.Fatalf("create server 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{s1.ID, s2.ID}
|
||||||
|
if err := ensureServersBelongToOrg(org.ID, ids, db); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureServersBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
orgA := models.Organization{Name: "org-a"}
|
||||||
|
orgB := models.Organization{Name: "org-b"}
|
||||||
|
|
||||||
|
if err := db.Create(&orgA).Error; err != nil {
|
||||||
|
t.Fatalf("create orgA: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&orgB).Error; err != nil {
|
||||||
|
t.Fatalf("create orgB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyA := createTestSshKey(t, db, orgA.ID, "org-a-key")
|
||||||
|
sshKeyB := createTestSshKey(t, db, orgB.ID, "org-b-key")
|
||||||
|
|
||||||
|
s1 := models.Server{
|
||||||
|
OrganizationID: orgA.ID,
|
||||||
|
Hostname: "srv-a-1",
|
||||||
|
SSHUser: "ubuntu",
|
||||||
|
SshKeyID: sshKeyA.ID,
|
||||||
|
Role: "worker",
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
s2 := models.Server{
|
||||||
|
OrganizationID: orgB.ID,
|
||||||
|
Hostname: "srv-b-1",
|
||||||
|
SSHUser: "ubuntu",
|
||||||
|
SshKeyID: sshKeyB.ID,
|
||||||
|
Role: "worker",
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&s1).Error; err != nil {
|
||||||
|
t.Fatalf("create server s1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&s2).Error; err != nil {
|
||||||
|
t.Fatalf("create server s2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{s1.ID, s2.ID}
|
||||||
|
if err := ensureServersBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||||
|
t.Fatalf("expected error when one server belongs to a different org")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ensureTaintsBelongToOrg ---
|
||||||
|
|
||||||
|
func TestEnsureTaintsBelongToOrg_AllBelong(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
org := models.Organization{Name: "org-taints"}
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
t.Fatalf("create org: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t1 := models.Taint{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
Key: "key1",
|
||||||
|
Value: "val1",
|
||||||
|
Effect: "NoSchedule",
|
||||||
|
}
|
||||||
|
t2 := models.Taint{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
Key: "key2",
|
||||||
|
Value: "val2",
|
||||||
|
Effect: "PreferNoSchedule",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&t1).Error; err != nil {
|
||||||
|
t.Fatalf("create taint 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&t2).Error; err != nil {
|
||||||
|
t.Fatalf("create taint 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{t1.ID, t2.ID}
|
||||||
|
if err := ensureTaintsBelongToOrg(org.ID, ids, db); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureTaintsBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
orgA := models.Organization{Name: "org-a"}
|
||||||
|
orgB := models.Organization{Name: "org-b"}
|
||||||
|
if err := db.Create(&orgA).Error; err != nil {
|
||||||
|
t.Fatalf("create orgA: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&orgB).Error; err != nil {
|
||||||
|
t.Fatalf("create orgB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t1 := models.Taint{
|
||||||
|
OrganizationID: orgA.ID,
|
||||||
|
Key: "key1",
|
||||||
|
Value: "val1",
|
||||||
|
Effect: "NoSchedule",
|
||||||
|
}
|
||||||
|
t2 := models.Taint{
|
||||||
|
OrganizationID: orgB.ID,
|
||||||
|
Key: "key2",
|
||||||
|
Value: "val2",
|
||||||
|
Effect: "NoSchedule",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&t1).Error; err != nil {
|
||||||
|
t.Fatalf("create taint 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&t2).Error; err != nil {
|
||||||
|
t.Fatalf("create taint 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{t1.ID, t2.ID}
|
||||||
|
if err := ensureTaintsBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||||
|
t.Fatalf("expected error when a taint belongs to another org")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ensureLabelsBelongToOrg ---
|
||||||
|
|
||||||
|
func TestEnsureLabelsBelongToOrg_AllBelong(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
org := models.Organization{Name: "org-labels"}
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
t.Fatalf("create org: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l1 := models.Label{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
},
|
||||||
|
Key: "env",
|
||||||
|
Value: "dev",
|
||||||
|
}
|
||||||
|
l2 := models.Label{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
},
|
||||||
|
Key: "env",
|
||||||
|
Value: "prod",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&l1).Error; err != nil {
|
||||||
|
t.Fatalf("create label 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&l2).Error; err != nil {
|
||||||
|
t.Fatalf("create label 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{l1.ID, l2.ID}
|
||||||
|
if err := ensureLabelsBelongToOrg(org.ID, ids, db); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureLabelsBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
orgA := models.Organization{Name: "org-a"}
|
||||||
|
orgB := models.Organization{Name: "org-b"}
|
||||||
|
if err := db.Create(&orgA).Error; err != nil {
|
||||||
|
t.Fatalf("create orgA: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&orgB).Error; err != nil {
|
||||||
|
t.Fatalf("create orgB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l1 := models.Label{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: orgA.ID,
|
||||||
|
},
|
||||||
|
Key: "env",
|
||||||
|
Value: "dev",
|
||||||
|
}
|
||||||
|
l2 := models.Label{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: orgB.ID,
|
||||||
|
},
|
||||||
|
Key: "env",
|
||||||
|
Value: "prod",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&l1).Error; err != nil {
|
||||||
|
t.Fatalf("create label 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&l2).Error; err != nil {
|
||||||
|
t.Fatalf("create label 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{l1.ID, l2.ID}
|
||||||
|
if err := ensureLabelsBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||||
|
t.Fatalf("expected error when a label belongs to another org")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ensureAnnotaionsBelongToOrg (typo in original name is preserved) ---
|
||||||
|
|
||||||
|
func TestEnsureAnnotationsBelongToOrg_AllBelong(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
org := models.Organization{Name: "org-annotations"}
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
t.Fatalf("create org: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a1 := models.Annotation{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
},
|
||||||
|
Key: "team",
|
||||||
|
Value: "core",
|
||||||
|
}
|
||||||
|
a2 := models.Annotation{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
},
|
||||||
|
Key: "team",
|
||||||
|
Value: "platform",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&a1).Error; err != nil {
|
||||||
|
t.Fatalf("create annotation 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&a2).Error; err != nil {
|
||||||
|
t.Fatalf("create annotation 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{a1.ID, a2.ID}
|
||||||
|
if err := ensureAnnotaionsBelongToOrg(org.ID, ids, db); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureAnnotationsBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
orgA := models.Organization{Name: "org-a"}
|
||||||
|
orgB := models.Organization{Name: "org-b"}
|
||||||
|
if err := db.Create(&orgA).Error; err != nil {
|
||||||
|
t.Fatalf("create orgA: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&orgB).Error; err != nil {
|
||||||
|
t.Fatalf("create orgB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a1 := models.Annotation{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: orgA.ID,
|
||||||
|
},
|
||||||
|
Key: "team",
|
||||||
|
Value: "core",
|
||||||
|
}
|
||||||
|
a2 := models.Annotation{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: orgB.ID,
|
||||||
|
},
|
||||||
|
Key: "team",
|
||||||
|
Value: "platform",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&a1).Error; err != nil {
|
||||||
|
t.Fatalf("create annotation 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&a2).Error; err != nil {
|
||||||
|
t.Fatalf("create annotation 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{a1.ID, a2.ID}
|
||||||
|
if err := ensureAnnotaionsBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||||
|
t.Fatalf("expected error when an annotation belongs to another org")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestSshKey(t *testing.T, db *gorm.DB, orgID uuid.UUID, name string) models.SshKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
key := models.SshKey{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey",
|
||||||
|
EncryptedPrivateKey: "encrypted",
|
||||||
|
PrivateIV: "iv",
|
||||||
|
PrivateTag: "tag",
|
||||||
|
Fingerprint: "fp-" + name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&key).Error; err != nil {
|
||||||
|
t.Fatalf("create ssh key %s: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
@@ -585,13 +585,22 @@ func CreateOrgKey(db *gorm.DB) http.HandlerFunc {
|
|||||||
exp = &e
|
exp = &e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix := orgKey
|
||||||
|
if len(prefix) > 12 {
|
||||||
|
prefix = prefix[:12]
|
||||||
|
}
|
||||||
|
|
||||||
rec := models.APIKey{
|
rec := models.APIKey{
|
||||||
OrgID: &oid,
|
OrgID: &oid,
|
||||||
Scope: "org",
|
Scope: "org",
|
||||||
Name: req.Name,
|
Purpose: "user",
|
||||||
KeyHash: keyHash,
|
IsEphemeral: false,
|
||||||
SecretHash: &secretHash,
|
Name: req.Name,
|
||||||
ExpiresAt: exp,
|
KeyHash: keyHash,
|
||||||
|
SecretHash: &secretHash,
|
||||||
|
ExpiresAt: exp,
|
||||||
|
Revoked: false,
|
||||||
|
Prefix: &prefix,
|
||||||
}
|
}
|
||||||
if err := db.Create(&rec).Error; err != nil {
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
utils.WriteError(w, 500, "db_error", err.Error())
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
// @Summary List servers (org scoped)
|
// @Summary List servers (org scoped)
|
||||||
// @Description Returns servers for the organization in X-Org-ID. Optional filters: status, role.
|
// @Description Returns servers for the organization in X-Org-ID. Optional filters: status, role.
|
||||||
// @Tags Servers
|
// @Tags Servers
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param status query string false "Filter by status (pending|provisioning|ready|failed)"
|
// @Param status query string false "Filter by status (pending|provisioning|ready|failed)"
|
||||||
@@ -89,7 +88,6 @@ func ListServers(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Get server by ID (org scoped)
|
// @Summary Get server by ID (org scoped)
|
||||||
// @Description Returns one server in the given organization.
|
// @Description Returns one server in the given organization.
|
||||||
// @Tags Servers
|
// @Tags Servers
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Server ID (UUID)"
|
// @Param id path string true "Server ID (UUID)"
|
||||||
@@ -329,11 +327,10 @@ func UpdateServer(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Delete server (org scoped)
|
// @Summary Delete server (org scoped)
|
||||||
// @Description Permanently deletes the server.
|
// @Description Permanently deletes the server.
|
||||||
// @Tags Servers
|
// @Tags Servers
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Server ID (UUID)"
|
// @Param id path string true "Server ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
|
|||||||
78
internal/handlers/servers_test.go
Normal file
78
internal/handlers/servers_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/testutil/pgtest"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidStatus(t *testing.T) {
|
||||||
|
// known-good statuses from servers.go
|
||||||
|
valid := []string{"pending", "provisioning", "ready", "failed"}
|
||||||
|
for _, s := range valid {
|
||||||
|
if !validStatus(s) {
|
||||||
|
t.Errorf("expected validStatus(%q) = true, got false", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := []string{"foobar", "unknown"}
|
||||||
|
for _, s := range invalid {
|
||||||
|
if validStatus(s) {
|
||||||
|
t.Errorf("expected validStatus(%q) = false, got true", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureKeyBelongsToOrg_Success(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
org := models.Organization{Name: "servers-org"}
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
t.Fatalf("create org: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := createTestSshKey(t, db, org.ID, "org-key")
|
||||||
|
|
||||||
|
if err := ensureKeyBelongsToOrg(org.ID, key.ID, db); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureKeyBelongsToOrg_WrongOrg(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
orgA := models.Organization{Name: "org-a"}
|
||||||
|
orgB := models.Organization{Name: "org-b"}
|
||||||
|
|
||||||
|
if err := db.Create(&orgA).Error; err != nil {
|
||||||
|
t.Fatalf("create orgA: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&orgB).Error; err != nil {
|
||||||
|
t.Fatalf("create orgB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyA := createTestSshKey(t, db, orgA.ID, "org-a-key")
|
||||||
|
|
||||||
|
// ask for orgB with a key that belongs to orgA → should fail
|
||||||
|
if err := ensureKeyBelongsToOrg(orgB.ID, keyA.ID, db); err == nil {
|
||||||
|
t.Fatalf("expected error when ssh key belongs to a different org, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureKeyBelongsToOrg_NotFound(t *testing.T) {
|
||||||
|
db := pgtest.DB(t)
|
||||||
|
|
||||||
|
org := models.Organization{Name: "org-nokey"}
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
t.Fatalf("create org: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// random keyID that doesn't exist
|
||||||
|
randomKeyID := uuid.New()
|
||||||
|
|
||||||
|
if err := ensureKeyBelongsToOrg(org.ID, randomKeyID, db); err == nil {
|
||||||
|
t.Fatalf("expected error when ssh key does not exist, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ import (
|
|||||||
// @Summary List ssh keys (org scoped)
|
// @Summary List ssh keys (org scoped)
|
||||||
// @Description Returns ssh keys for the organization in X-Org-ID.
|
// @Description Returns ssh keys for the organization in X-Org-ID.
|
||||||
// @Tags Ssh
|
// @Tags Ssh
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Success 200 {array} dto.SshResponse
|
// @Success 200 {array} dto.SshResponse
|
||||||
@@ -189,7 +188,6 @@ func CreateSSHKey(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Get ssh key by ID (org scoped)
|
// @Summary Get ssh key by ID (org scoped)
|
||||||
// @Description Returns public key fields. Append `?reveal=true` to include the private key PEM.
|
// @Description Returns public key fields. Append `?reveal=true` to include the private key PEM.
|
||||||
// @Tags Ssh
|
// @Tags Ssh
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "SSH Key ID (UUID)"
|
// @Param id path string true "SSH Key ID (UUID)"
|
||||||
@@ -283,11 +281,10 @@ func GetSSHKey(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Delete ssh keypair (org scoped)
|
// @Summary Delete ssh keypair (org scoped)
|
||||||
// @Description Permanently deletes a keypair.
|
// @Description Permanently deletes a keypair.
|
||||||
// @Tags Ssh
|
// @Tags Ssh
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "SSH Key ID (UUID)"
|
// @Param id path string true "SSH Key ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
// @Summary List node pool taints (org scoped)
|
// @Summary List node pool taints (org scoped)
|
||||||
// @Description Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.
|
// @Description Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.
|
||||||
// @Tags Taints
|
// @Tags Taints
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param key query string false "Exact key"
|
// @Param key query string false "Exact key"
|
||||||
@@ -70,7 +69,6 @@ func ListTaints(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @ID GetTaint
|
// @ID GetTaint
|
||||||
// @Summary Get node taint by ID (org scoped)
|
// @Summary Get node taint by ID (org scoped)
|
||||||
// @Tags Taints
|
// @Tags Taints
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Taint ID (UUID)"
|
// @Param id path string true "Node Taint ID (UUID)"
|
||||||
@@ -279,11 +277,10 @@ func UpdateTaint(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @Summary Delete taint (org scoped)
|
// @Summary Delete taint (org scoped)
|
||||||
// @Description Permanently deletes the taint.
|
// @Description Permanently deletes the taint.
|
||||||
// @Tags Taints
|
// @Tags Taints
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string false "Organization UUID"
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
// @Param id path string true "Node Taint ID (UUID)"
|
// @Param id path string true "Node Taint ID (UUID)"
|
||||||
// @Success 204 {string} string "No Content"
|
// @Success 204 "No Content"
|
||||||
// @Failure 400 {string} string "invalid id"
|
// @Failure 400 {string} string "invalid id"
|
||||||
// @Failure 401 {string} string "Unauthorized"
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 403 {string} string "organization required"
|
// @Failure 403 {string} string "organization required"
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ type VersionResponse struct {
|
|||||||
// @Description Returns build/runtime metadata for the running service.
|
// @Description Returns build/runtime metadata for the running service.
|
||||||
// @Tags Meta
|
// @Tags Meta
|
||||||
// @ID Version // operationId
|
// @ID Version // operationId
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} VersionResponse
|
// @Success 200 {object} VersionResponse
|
||||||
// @Router /version [get]
|
// @Router /version [get]
|
||||||
|
|||||||
182
internal/mapper/cluster.go
Normal file
182
internal/mapper/cluster.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/common"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClusterToDTO(c models.Cluster) dto.ClusterResponse {
|
||||||
|
var bastion *dto.ServerResponse
|
||||||
|
if c.BastionServer != nil {
|
||||||
|
b := ServerToDTO(*c.BastionServer)
|
||||||
|
bastion = &b
|
||||||
|
}
|
||||||
|
|
||||||
|
var captainDomain *dto.DomainResponse
|
||||||
|
if c.CaptainDomainID != nil && c.CaptainDomain.ID != uuid.Nil {
|
||||||
|
dr := DomainToDTO(c.CaptainDomain)
|
||||||
|
captainDomain = &dr
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlPlane *dto.RecordSetResponse
|
||||||
|
if c.ControlPlaneRecordSet != nil {
|
||||||
|
rr := RecordSetToDTO(*c.ControlPlaneRecordSet)
|
||||||
|
controlPlane = &rr
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfqdn *string
|
||||||
|
if captainDomain != nil && controlPlane != nil {
|
||||||
|
fq := fmt.Sprintf("%s.%s", controlPlane.Name, captainDomain.DomainName)
|
||||||
|
cfqdn = &fq
|
||||||
|
}
|
||||||
|
|
||||||
|
var appsLB *dto.LoadBalancerResponse
|
||||||
|
if c.AppsLoadBalancer != nil {
|
||||||
|
lr := LoadBalancerToDTO(*c.AppsLoadBalancer)
|
||||||
|
appsLB = &lr
|
||||||
|
}
|
||||||
|
|
||||||
|
var glueOpsLB *dto.LoadBalancerResponse
|
||||||
|
if c.GlueOpsLoadBalancer != nil {
|
||||||
|
lr := LoadBalancerToDTO(*c.GlueOpsLoadBalancer)
|
||||||
|
glueOpsLB = &lr
|
||||||
|
}
|
||||||
|
|
||||||
|
nps := make([]dto.NodePoolResponse, 0, len(c.NodePools))
|
||||||
|
for _, np := range c.NodePools {
|
||||||
|
nps = append(nps, NodePoolToDTO(np))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.ClusterResponse{
|
||||||
|
ID: c.ID,
|
||||||
|
Name: c.Name,
|
||||||
|
CaptainDomain: captainDomain,
|
||||||
|
ControlPlaneRecordSet: controlPlane,
|
||||||
|
ControlPlaneFQDN: cfqdn,
|
||||||
|
AppsLoadBalancer: appsLB,
|
||||||
|
GlueOpsLoadBalancer: glueOpsLB,
|
||||||
|
BastionServer: bastion,
|
||||||
|
Provider: c.Provider,
|
||||||
|
Region: c.Region,
|
||||||
|
Status: c.Status,
|
||||||
|
LastError: c.LastError,
|
||||||
|
RandomToken: c.RandomToken,
|
||||||
|
CertificateKey: c.CertificateKey,
|
||||||
|
NodePools: nps,
|
||||||
|
DockerImage: c.DockerImage,
|
||||||
|
DockerTag: c.DockerTag,
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
UpdatedAt: c.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NodePoolToDTO(np models.NodePool) dto.NodePoolResponse {
|
||||||
|
labels := make([]dto.LabelResponse, 0, len(np.Labels))
|
||||||
|
for _, l := range np.Labels {
|
||||||
|
labels = append(labels, dto.LabelResponse{
|
||||||
|
Key: l.Key,
|
||||||
|
Value: l.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations := make([]dto.AnnotationResponse, 0, len(np.Annotations))
|
||||||
|
for _, a := range np.Annotations {
|
||||||
|
annotations = append(annotations, dto.AnnotationResponse{
|
||||||
|
Key: a.Key,
|
||||||
|
Value: a.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
taints := make([]dto.TaintResponse, 0, len(np.Taints))
|
||||||
|
for _, t := range np.Taints {
|
||||||
|
taints = append(taints, dto.TaintResponse{
|
||||||
|
Key: t.Key,
|
||||||
|
Value: t.Value,
|
||||||
|
Effect: t.Effect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
servers := make([]dto.ServerResponse, 0, len(np.Servers))
|
||||||
|
for _, s := range np.Servers {
|
||||||
|
servers = append(servers, ServerToDTO(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.NodePoolResponse{
|
||||||
|
AuditFields: common.AuditFields{
|
||||||
|
ID: np.ID,
|
||||||
|
OrganizationID: np.OrganizationID,
|
||||||
|
CreatedAt: np.CreatedAt,
|
||||||
|
UpdatedAt: np.UpdatedAt,
|
||||||
|
},
|
||||||
|
Name: np.Name,
|
||||||
|
Role: dto.NodeRole(np.Role),
|
||||||
|
Labels: labels,
|
||||||
|
Annotations: annotations,
|
||||||
|
Taints: taints,
|
||||||
|
Servers: servers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerToDTO(s models.Server) dto.ServerResponse {
|
||||||
|
return dto.ServerResponse{
|
||||||
|
ID: s.ID,
|
||||||
|
Hostname: s.Hostname,
|
||||||
|
PrivateIPAddress: s.PrivateIPAddress,
|
||||||
|
PublicIPAddress: s.PublicIPAddress,
|
||||||
|
Role: s.Role,
|
||||||
|
Status: s.Status,
|
||||||
|
SSHUser: s.SSHUser,
|
||||||
|
SshKeyID: s.SshKeyID,
|
||||||
|
CreatedAt: s.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: s.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DomainToDTO(d models.Domain) dto.DomainResponse {
|
||||||
|
return dto.DomainResponse{
|
||||||
|
ID: d.ID.String(),
|
||||||
|
OrganizationID: d.OrganizationID.String(),
|
||||||
|
DomainName: d.DomainName,
|
||||||
|
ZoneID: d.ZoneID,
|
||||||
|
Status: d.Status,
|
||||||
|
LastError: d.LastError,
|
||||||
|
CredentialID: d.CredentialID.String(),
|
||||||
|
CreatedAt: d.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: d.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordSetToDTO(rs models.RecordSet) dto.RecordSetResponse {
|
||||||
|
return dto.RecordSetResponse{
|
||||||
|
ID: rs.ID.String(),
|
||||||
|
DomainID: rs.DomainID.String(),
|
||||||
|
Name: rs.Name,
|
||||||
|
Type: rs.Type,
|
||||||
|
TTL: rs.TTL,
|
||||||
|
Values: []byte(rs.Values),
|
||||||
|
Fingerprint: rs.Fingerprint,
|
||||||
|
Status: rs.Status,
|
||||||
|
Owner: rs.Owner,
|
||||||
|
LastError: rs.LastError,
|
||||||
|
CreatedAt: rs.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: rs.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadBalancerToDTO(lb models.LoadBalancer) dto.LoadBalancerResponse {
|
||||||
|
return dto.LoadBalancerResponse{
|
||||||
|
ID: lb.ID,
|
||||||
|
OrganizationID: lb.OrganizationID,
|
||||||
|
Name: lb.Name,
|
||||||
|
Kind: lb.Kind,
|
||||||
|
PublicIPAddress: lb.PublicIPAddress,
|
||||||
|
PrivateIPAddress: lb.PrivateIPAddress,
|
||||||
|
CreatedAt: lb.CreatedAt,
|
||||||
|
UpdatedAt: lb.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
16
internal/models/action.go
Normal file
16
internal/models/action.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
Label string `gorm:"type:varchar(255);not null;uniqueIndex" json:"label"`
|
||||||
|
Description string `gorm:"type:text;not null" json:"description"`
|
||||||
|
MakeTarget string `gorm:"type:varchar(255);not null;uniqueIndex" json:"make_target"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||||
|
}
|
||||||
@@ -7,17 +7,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIKey struct {
|
type APIKey struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
Name string `gorm:"not null;default:''" json:"name"`
|
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
|
||||||
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
|
Scope string `gorm:"not null;default:''" json:"scope"`
|
||||||
Scope string `gorm:"not null;default:''" json:"scope"`
|
Purpose string `json:"purpose"`
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
|
ClusterID *uuid.UUID `json:"cluster_id,omitempty"`
|
||||||
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
|
IsEphemeral bool `json:"is_ephemeral"`
|
||||||
SecretHash *string `json:"-"`
|
Name string `gorm:"not null;default:''" json:"name"`
|
||||||
ExpiresAt *time.Time `json:"expires_at,omitempty" format:"date-time"`
|
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||||
Revoked bool `gorm:"not null;default:false" json:"revoked"`
|
SecretHash *string `json:"-"`
|
||||||
Prefix *string `json:"prefix,omitempty"`
|
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
|
||||||
LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"`
|
ExpiresAt *time.Time `json:"expires_at,omitempty" format:"date-time"`
|
||||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
Revoked bool `gorm:"not null;default:false" json:"revoked"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
Prefix *string `json:"prefix,omitempty"`
|
||||||
|
LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,28 +6,43 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClusterStatusPrePending = "pre_pending" // needs validation
|
||||||
|
ClusterStatusIncomplete = "incomplete" // invalid/missing shape
|
||||||
|
ClusterStatusPending = "pending" // valid shape, waiting for provisioning
|
||||||
|
ClusterStatusProvisioning = "provisioning"
|
||||||
|
ClusterStatusReady = "ready"
|
||||||
|
ClusterStatusFailed = "failed" // provisioning/runtime failure
|
||||||
|
ClusterStatusBootstrapping = "bootstrapping"
|
||||||
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Status string `json:"status"`
|
Status string `gorm:"type:varchar(20);not null;default:'pre_pending'" json:"status"`
|
||||||
CaptainDomain string `gorm:"not null" json:"captain_domain"` // nonprod.earth.onglueops.rocks
|
LastError string `gorm:"type:text;not null;default:''" json:"last_error"`
|
||||||
AppsLoadBalancer string `json:"cluster_load_balancer"` // {public_ip: 1.2.3.4, private_ip: 10.0.30.1, name: apps.CaqptainDomain}
|
CaptainDomainID *uuid.UUID `gorm:"type:uuid" json:"captain_domain_id"`
|
||||||
GlueOpsLoadBalancer string `json:"control_load_balancer"` // {public_ip: 5.6.7.8, private_ip: 10.0.22.1, name: CaptainDomain}
|
CaptainDomain Domain `gorm:"foreignKey:CaptainDomainID" json:"captain_domain"`
|
||||||
|
ControlPlaneRecordSetID *uuid.UUID `gorm:"type:uuid" json:"control_plane_record_set_id,omitempty"`
|
||||||
ControlPlane string `json:"control_plane"` // <- dns cntlpn
|
ControlPlaneRecordSet *RecordSet `gorm:"foreignKey:ControlPlaneRecordSetID" json:"control_plane_record_set,omitempty"`
|
||||||
|
AppsLoadBalancerID *uuid.UUID `gorm:"type:uuid" json:"apps_load_balancer_id,omitempty"`
|
||||||
RandomToken string `json:"random_token"`
|
AppsLoadBalancer *LoadBalancer `gorm:"foreignKey:AppsLoadBalancerID" json:"apps_load_balancer,omitempty"`
|
||||||
CertificateKey string `json:"certificate_key"`
|
GlueOpsLoadBalancerID *uuid.UUID `gorm:"type:uuid" json:"glueops_load_balancer_id,omitempty"`
|
||||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
GlueOpsLoadBalancer *LoadBalancer `gorm:"foreignKey:GlueOpsLoadBalancerID" json:"glueops_load_balancer,omitempty"`
|
||||||
KubeIV string `json:"-"`
|
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||||
KubeTag string `json:"-"`
|
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
||||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
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"`
|
RandomToken string `json:"random_token"`
|
||||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
CertificateKey string `json:"certificate_key"`
|
||||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
KubeIV string `json:"-"`
|
||||||
|
KubeTag string `json:"-"`
|
||||||
|
DockerImage string `json:"docker_image"`
|
||||||
|
DockerTag string `json:"docker_tag"`
|
||||||
|
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()"`
|
||||||
}
|
}
|
||||||
|
|||||||
27
internal/models/cluster_runs.go
Normal file
27
internal/models/cluster_runs.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClusterRunStatusQueued = "queued"
|
||||||
|
ClusterRunStatusRunning = "running"
|
||||||
|
ClusterRunStatusSuccess = "success"
|
||||||
|
ClusterRunStatusFailed = "failed"
|
||||||
|
ClusterRunStatusCanceled = "canceled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterRun struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" gorm:"type:uuid;index"`
|
||||||
|
ClusterID uuid.UUID `json:"cluster_id" gorm:"type:uuid;index"`
|
||||||
|
Action string `json:"action" gorm:"type:text;not null"`
|
||||||
|
Status string `json:"status" gorm:"type:text;not null"`
|
||||||
|
Error string `json:"error" gorm:"type:text;not null"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||||
|
FinishedAt time.Time `json:"finished_at,omitempty" gorm:"type:timestamptz" format:"date-time"`
|
||||||
|
}
|
||||||
19
internal/models/load_balancer.go
Normal file
19
internal/models/load_balancer.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoadBalancer struct {
|
||||||
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" gorm:"type:uuid;index"`
|
||||||
|
Organization Organization `json:"organization" gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE"`
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
Kind string `json:"kind" gorm:"not null"`
|
||||||
|
PublicIPAddress string `json:"public_ip_address" gorm:"not null"`
|
||||||
|
PrivateIPAddress string `json:"private_ip_address" gorm:"not null"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
119
internal/testutil/pgtest/pgtest.go
Normal file
119
internal/testutil/pgtest/pgtest.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package pgtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
||||||
|
"github.com/glueops/autoglue/internal/db"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
epg *embeddedpostgres.EmbeddedPostgres
|
||||||
|
gdb *gorm.DB
|
||||||
|
initErr error
|
||||||
|
dsn string
|
||||||
|
)
|
||||||
|
|
||||||
|
// initDB is called once via sync.Once. It starts embedded Postgres,
|
||||||
|
// opens a GORM connection and runs the same migrations as NewRuntime.
|
||||||
|
func initDB() {
|
||||||
|
const port uint32 = 55432
|
||||||
|
|
||||||
|
cfg := embeddedpostgres.
|
||||||
|
DefaultConfig().
|
||||||
|
Database("autoglue_test").
|
||||||
|
Username("autoglue").
|
||||||
|
Password("autoglue").
|
||||||
|
Port(port).
|
||||||
|
StartTimeout(30 * time.Second)
|
||||||
|
|
||||||
|
epg = embeddedpostgres.NewDatabase(cfg)
|
||||||
|
if err := epg.Start(); err != nil {
|
||||||
|
initErr = fmt.Errorf("start embedded postgres: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn = fmt.Sprintf(
|
||||||
|
"host=127.0.0.1 port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
port,
|
||||||
|
"autoglue",
|
||||||
|
"autoglue",
|
||||||
|
"autoglue_test",
|
||||||
|
)
|
||||||
|
|
||||||
|
dbConn, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
initErr = fmt.Errorf("open gorm: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the same model list as app.NewRuntime so schema matches prod
|
||||||
|
if err := db.Run(
|
||||||
|
dbConn,
|
||||||
|
&models.Job{},
|
||||||
|
&models.MasterKey{},
|
||||||
|
&models.SigningKey{},
|
||||||
|
&models.User{},
|
||||||
|
&models.Organization{},
|
||||||
|
&models.Account{},
|
||||||
|
&models.Membership{},
|
||||||
|
&models.APIKey{},
|
||||||
|
&models.UserEmail{},
|
||||||
|
&models.RefreshToken{},
|
||||||
|
&models.OrganizationKey{},
|
||||||
|
&models.SshKey{},
|
||||||
|
&models.Server{},
|
||||||
|
&models.Taint{},
|
||||||
|
&models.Label{},
|
||||||
|
&models.Annotation{},
|
||||||
|
&models.NodePool{},
|
||||||
|
&models.Cluster{},
|
||||||
|
&models.Credential{},
|
||||||
|
&models.Domain{},
|
||||||
|
&models.RecordSet{},
|
||||||
|
); err != nil {
|
||||||
|
initErr = fmt.Errorf("migrate: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gdb = dbConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB returns a lazily-initialized *gorm.DB backed by embedded Postgres.
|
||||||
|
//
|
||||||
|
// Call this from any test that needs a real DB. If init fails, the test
|
||||||
|
// will fail immediately with a clear message.
|
||||||
|
func DB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
once.Do(initDB)
|
||||||
|
if initErr != nil {
|
||||||
|
t.Fatalf("failed to init embedded postgres: %v", initErr)
|
||||||
|
}
|
||||||
|
return gdb
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL returns the DSN for the embedded Postgres instance, useful for code
|
||||||
|
// that expects a DB URL (e.g. bg.NewJobs).
|
||||||
|
func URL(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
DB(t) // ensure initialized
|
||||||
|
return dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the embedded Postgres process. Call from TestMain in at
|
||||||
|
// least one package, or let the OS clean it up on process exit.
|
||||||
|
func Stop() {
|
||||||
|
if epg != nil {
|
||||||
|
if err := epg.Stop(); err != nil {
|
||||||
|
log.Printf("stop embedded postgres: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
internal/web/dist/assets/index-52pog1DZ.js
vendored
79
internal/web/dist/assets/index-52pog1DZ.js
vendored
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-52pog1DZ.js.br
vendored
BIN
internal/web/dist/assets/index-52pog1DZ.js.br
vendored
Binary file not shown.
BIN
internal/web/dist/assets/index-52pog1DZ.js.gz
vendored
BIN
internal/web/dist/assets/index-52pog1DZ.js.gz
vendored
Binary file not shown.
File diff suppressed because one or more lines are too long
4167
internal/web/dist/assets/index-CyGsiYei.js
vendored
Normal file
4167
internal/web/dist/assets/index-CyGsiYei.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-CyGsiYei.js.br
vendored
Normal file
BIN
internal/web/dist/assets/index-CyGsiYei.js.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/index-CyGsiYei.js.gz
vendored
Normal file
BIN
internal/web/dist/assets/index-CyGsiYei.js.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/assets/index-CyGsiYei.js.map
vendored
Normal file
1
internal/web/dist/assets/index-CyGsiYei.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
internal/web/dist/assets/index-VHZG0dIU.css
vendored
Normal file
2
internal/web/dist/assets/index-VHZG0dIU.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-VHZG0dIU.css.br
vendored
Normal file
BIN
internal/web/dist/assets/index-VHZG0dIU.css.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/index-VHZG0dIU.css.gz
vendored
Normal file
BIN
internal/web/dist/assets/index-VHZG0dIU.css.gz
vendored
Normal file
Binary file not shown.
2
internal/web/dist/assets/index-tX4seA_J.css
vendored
2
internal/web/dist/assets/index-tX4seA_J.css
vendored
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-tX4seA_J.css.br
vendored
BIN
internal/web/dist/assets/index-tX4seA_J.css.br
vendored
Binary file not shown.
BIN
internal/web/dist/assets/index-tX4seA_J.css.gz
vendored
BIN
internal/web/dist/assets/index-tX4seA_J.css.gz
vendored
Binary file not shown.
4
internal/web/dist/assets/react-B75e6Si-.js
vendored
4
internal/web/dist/assets/react-B75e6Si-.js
vendored
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/react-B75e6Si-.js.br
vendored
BIN
internal/web/dist/assets/react-B75e6Si-.js.br
vendored
Binary file not shown.
BIN
internal/web/dist/assets/react-B75e6Si-.js.gz
vendored
BIN
internal/web/dist/assets/react-B75e6Si-.js.gz
vendored
Binary file not shown.
File diff suppressed because one or more lines are too long
4
internal/web/dist/assets/react-Dt2M6tWj.js
vendored
Normal file
4
internal/web/dist/assets/react-Dt2M6tWj.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/react-Dt2M6tWj.js.br
vendored
Normal file
BIN
internal/web/dist/assets/react-Dt2M6tWj.js.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/react-Dt2M6tWj.js.gz
vendored
Normal file
BIN
internal/web/dist/assets/react-Dt2M6tWj.js.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/assets/react-Dt2M6tWj.js.map
vendored
Normal file
1
internal/web/dist/assets/react-Dt2M6tWj.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
internal/web/dist/index.html
vendored
6
internal/web/dist/index.html
vendored
@@ -5,9 +5,9 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AutoGlue</title>
|
<title>AutoGlue</title>
|
||||||
<script type="module" crossorigin src="/assets/index-52pog1DZ.js"></script>
|
<script type="module" crossorigin src="/assets/index-CyGsiYei.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/react-B75e6Si-.js">
|
<link rel="modulepreload" crossorigin href="/assets/react-Dt2M6tWj.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-tX4seA_J.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-VHZG0dIU.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
BIN
internal/web/dist/index.html.br
vendored
BIN
internal/web/dist/index.html.br
vendored
Binary file not shown.
BIN
internal/web/dist/index.html.gz
vendored
BIN
internal/web/dist/index.html.gz
vendored
Binary file not shown.
@@ -67,7 +67,13 @@ func SPAHandler() (http.Handler, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
raw := strings.TrimSpace(r.URL.Path)
|
||||||
|
if raw == "" || raw == "/" {
|
||||||
|
raw = "/index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
clean := path.Clean("/" + raw) // nosemgrep: autoglue.filesystem.no-path-clean
|
||||||
|
filePath := strings.TrimPrefix(clean, "/")
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
filePath = "index.html"
|
filePath = "index.html"
|
||||||
}
|
}
|
||||||
|
|||||||
58
main.go
58
main.go
@@ -1,44 +1,46 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/glueops/autoglue/cmd"
|
"github.com/glueops/autoglue/cmd"
|
||||||
"github.com/glueops/autoglue/docs"
|
"github.com/glueops/autoglue/docs"
|
||||||
"github.com/joho/godotenv"
|
"github.com/glueops/autoglue/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title AutoGlue API
|
// @title AutoGlue API
|
||||||
// @version 1.0
|
// @version dev
|
||||||
// @description API for managing K3s clusters across cloud providers
|
// @description API for managing K3s clusters across cloud providers
|
||||||
|
// @contact.name GlueOps
|
||||||
|
|
||||||
// @contact.name GlueOps
|
// @servers.url https://autoglue.glueopshosted.com/api/v1
|
||||||
|
// @servers.description Production API
|
||||||
|
// @servers.url https://autoglue.glueopshosted.rocks/api/v1
|
||||||
|
// @servers.description Pre-Production API
|
||||||
|
// @servers.url https://autoglue.apps.nonprod.earth.onglueops.rocks/api/v1
|
||||||
|
// @servers.description Staging API
|
||||||
|
// @servers.url http://localhost:8080/api/v1
|
||||||
|
// @servers.description Local dev
|
||||||
|
|
||||||
// @BasePath /api/v1
|
// @securityDefinitions.apikey BearerAuth
|
||||||
// @schemes http https
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @description Bearer token authentication
|
||||||
|
|
||||||
// @securityDefinitions.apikey BearerAuth
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name Authorization
|
// @name X-API-KEY
|
||||||
// @description Bearer token authentication
|
// @description User API key
|
||||||
|
|
||||||
// @securityDefinitions.apikey ApiKeyAuth
|
// @securityDefinitions.apikey OrgKeyAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name X-API-KEY
|
// @name X-ORG-KEY
|
||||||
// @description User API key
|
// @description Org-level key/secret authentication
|
||||||
|
|
||||||
// @securityDefinitions.apikey OrgKeyAuth
|
// @securityDefinitions.apikey OrgSecretAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name X-ORG-KEY
|
// @name X-ORG-SECRET
|
||||||
// @description Org-level key/secret authentication
|
// @description Org-level secret
|
||||||
|
|
||||||
// @securityDefinitions.apikey OrgSecretAuth
|
|
||||||
// @in header
|
|
||||||
// @name X-ORG-SECRET
|
|
||||||
// @description Org-level secret
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = godotenv.Load()
|
docs.SwaggerInfo.Version = version.Version
|
||||||
docs.SwaggerInfo.Host = os.Getenv("SWAGGER_HOST")
|
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ RUN cd /var/lib/postgresql/ && \
|
|||||||
openssl req -x509 -in server.req -text -key server.key -out server.crt && \
|
openssl req -x509 -in server.req -text -key server.key -out server.crt && \
|
||||||
chmod 600 server.key && \
|
chmod 600 server.key && \
|
||||||
chown postgres:postgres server.key
|
chown postgres:postgres server.key
|
||||||
|
|
||||||
CMD ["postgres", "-c", "ssl=on", "-c", "ssl_cert_file=/var/lib/postgresql/server.crt", "-c", "ssl_key_file=/var/lib/postgresql/server.key" ]
|
CMD ["postgres", "-c", "ssl=on", "-c", "ssl_cert_file=/var/lib/postgresql/server.crt", "-c", "ssl_key_file=/var/lib/postgresql/server.key" ]
|
||||||
|
|||||||
435
schema.sql
Normal file
435
schema.sql
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
-- Add new schema named "public"
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "public";
|
||||||
|
-- Set comment to schema: "public"
|
||||||
|
COMMENT ON SCHEMA "public" IS 'standard public schema';
|
||||||
|
-- Create "jobs" table
|
||||||
|
CREATE TABLE "public"."jobs" (
|
||||||
|
"id" character varying NOT NULL,
|
||||||
|
"queue_name" character varying NOT NULL,
|
||||||
|
"status" character varying NOT NULL,
|
||||||
|
"arguments" jsonb NOT NULL DEFAULT '{}',
|
||||||
|
"result" jsonb NOT NULL DEFAULT '{}',
|
||||||
|
"last_error" character varying NULL,
|
||||||
|
"retry_count" bigint NOT NULL DEFAULT 0,
|
||||||
|
"max_retry" bigint NOT NULL DEFAULT 0,
|
||||||
|
"retry_interval" bigint NOT NULL DEFAULT 0,
|
||||||
|
"scheduled_at" timestamptz NULL DEFAULT now(),
|
||||||
|
"started_at" timestamptz NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create index "idx_jobs_scheduled_at" to table: "jobs"
|
||||||
|
CREATE INDEX "idx_jobs_scheduled_at" ON "public"."jobs" ("scheduled_at");
|
||||||
|
-- Create index "idx_jobs_started_at" to table: "jobs"
|
||||||
|
CREATE INDEX "idx_jobs_started_at" ON "public"."jobs" ("started_at");
|
||||||
|
-- Create "api_keys" table
|
||||||
|
CREATE TABLE "public"."api_keys" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"name" text NOT NULL DEFAULT '',
|
||||||
|
"key_hash" text NOT NULL,
|
||||||
|
"scope" text NOT NULL DEFAULT '',
|
||||||
|
"user_id" text NULL,
|
||||||
|
"org_id" text NULL,
|
||||||
|
"secret_hash" text NULL,
|
||||||
|
"expires_at" timestamptz NULL,
|
||||||
|
"revoked" boolean NOT NULL DEFAULT false,
|
||||||
|
"prefix" text NULL,
|
||||||
|
"last_used_at" timestamptz NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create index "idx_api_keys_key_hash" to table: "api_keys"
|
||||||
|
CREATE UNIQUE INDEX "idx_api_keys_key_hash" ON "public"."api_keys" ("key_hash");
|
||||||
|
-- Create "refresh_tokens" table
|
||||||
|
CREATE TABLE "public"."refresh_tokens" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"family_id" uuid NOT NULL,
|
||||||
|
"token_hash" text NOT NULL,
|
||||||
|
"expires_at" timestamptz NOT NULL,
|
||||||
|
"revoked_at" timestamptz NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create index "idx_refresh_tokens_family_id" to table: "refresh_tokens"
|
||||||
|
CREATE INDEX "idx_refresh_tokens_family_id" ON "public"."refresh_tokens" ("family_id");
|
||||||
|
-- Create index "idx_refresh_tokens_token_hash" to table: "refresh_tokens"
|
||||||
|
CREATE UNIQUE INDEX "idx_refresh_tokens_token_hash" ON "public"."refresh_tokens" ("token_hash");
|
||||||
|
-- Create index "idx_refresh_tokens_user_id" to table: "refresh_tokens"
|
||||||
|
CREATE INDEX "idx_refresh_tokens_user_id" ON "public"."refresh_tokens" ("user_id");
|
||||||
|
-- Create "users" table
|
||||||
|
CREATE TABLE "public"."users" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"display_name" text NULL,
|
||||||
|
"primary_email" text NULL,
|
||||||
|
"avatar_url" text NULL,
|
||||||
|
"is_disabled" boolean NULL,
|
||||||
|
"is_admin" boolean NULL DEFAULT false,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create "signing_keys" table
|
||||||
|
CREATE TABLE "public"."signing_keys" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"kid" text NOT NULL,
|
||||||
|
"alg" text NOT NULL,
|
||||||
|
"use" text NOT NULL DEFAULT 'sig',
|
||||||
|
"is_active" boolean NOT NULL DEFAULT true,
|
||||||
|
"public_pem" text NOT NULL,
|
||||||
|
"private_pem" text NOT NULL,
|
||||||
|
"not_before" timestamptz NULL,
|
||||||
|
"expires_at" timestamptz NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"rotated_from" text NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create index "idx_signing_keys_kid" to table: "signing_keys"
|
||||||
|
CREATE UNIQUE INDEX "idx_signing_keys_kid" ON "public"."signing_keys" ("kid");
|
||||||
|
-- Create "accounts" table
|
||||||
|
CREATE TABLE "public"."accounts" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"provider" text NOT NULL,
|
||||||
|
"subject" text NOT NULL,
|
||||||
|
"email" text NULL,
|
||||||
|
"email_verified" boolean NOT NULL DEFAULT false,
|
||||||
|
"profile" jsonb NOT NULL DEFAULT '{}',
|
||||||
|
"secret_hash" text NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_accounts_user" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
-- Create index "idx_accounts_user_id" to table: "accounts"
|
||||||
|
CREATE INDEX "idx_accounts_user_id" ON "public"."accounts" ("user_id");
|
||||||
|
-- Create "organizations" table
|
||||||
|
CREATE TABLE "public"."organizations" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"domain" text NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create index "idx_organizations_domain" to table: "organizations"
|
||||||
|
CREATE INDEX "idx_organizations_domain" ON "public"."organizations" ("domain");
|
||||||
|
-- Create "annotations" table
|
||||||
|
CREATE TABLE "public"."annotations" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_annotations_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_annotations_organization_id" to table: "annotations"
|
||||||
|
CREATE INDEX "idx_annotations_organization_id" ON "public"."annotations" ("organization_id");
|
||||||
|
-- Create "credentials" table
|
||||||
|
CREATE TABLE "public"."credentials" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"provider" character varying(50) NOT NULL,
|
||||||
|
"kind" character varying(50) NOT NULL,
|
||||||
|
"scope_kind" character varying(20) NOT NULL,
|
||||||
|
"scope" jsonb NOT NULL DEFAULT '{}',
|
||||||
|
"scope_fingerprint" character(64) NOT NULL,
|
||||||
|
"schema_version" bigint NOT NULL DEFAULT 1,
|
||||||
|
"name" character varying(100) NOT NULL DEFAULT '',
|
||||||
|
"scope_version" bigint NOT NULL DEFAULT 1,
|
||||||
|
"account_id" character varying(32) NULL,
|
||||||
|
"region" character varying(32) NULL,
|
||||||
|
"encrypted_data" text NOT NULL,
|
||||||
|
"iv" text NOT NULL,
|
||||||
|
"tag" text NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_credentials_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_credentials_organization_id" to table: "credentials"
|
||||||
|
CREATE INDEX "idx_credentials_organization_id" ON "public"."credentials" ("organization_id");
|
||||||
|
-- Create index "idx_credentials_scope_fingerprint" to table: "credentials"
|
||||||
|
CREATE INDEX "idx_credentials_scope_fingerprint" ON "public"."credentials" ("scope_fingerprint");
|
||||||
|
-- Create index "idx_kind_scope" to table: "credentials"
|
||||||
|
CREATE INDEX "idx_kind_scope" ON "public"."credentials" ("kind", "scope");
|
||||||
|
-- Create index "idx_provider_kind" to table: "credentials"
|
||||||
|
CREATE INDEX "idx_provider_kind" ON "public"."credentials" ("provider", "kind");
|
||||||
|
-- Create index "uniq_org_provider_scopekind_scope" to table: "credentials"
|
||||||
|
CREATE UNIQUE INDEX "uniq_org_provider_scopekind_scope" ON "public"."credentials" ("organization_id", "provider", "scope_kind", "scope_fingerprint");
|
||||||
|
-- Create "backups" table
|
||||||
|
CREATE TABLE "public"."backups" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"enabled" boolean NOT NULL DEFAULT false,
|
||||||
|
"credential_id" uuid NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_backups_credential" FOREIGN KEY ("credential_id") REFERENCES "public"."credentials" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_backups_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_backups_organization_id" to table: "backups"
|
||||||
|
CREATE INDEX "idx_backups_organization_id" ON "public"."backups" ("organization_id");
|
||||||
|
-- Create index "uniq_org_credential" to table: "backups"
|
||||||
|
CREATE UNIQUE INDEX "uniq_org_credential" ON "public"."backups" ("organization_id", "credential_id");
|
||||||
|
-- Create "load_balancers" table
|
||||||
|
CREATE TABLE "public"."load_balancers" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"public_ip_address" text NOT NULL,
|
||||||
|
"private_ip_address" text NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_load_balancers_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_load_balancers_organization_id" to table: "load_balancers"
|
||||||
|
CREATE INDEX "idx_load_balancers_organization_id" ON "public"."load_balancers" ("organization_id");
|
||||||
|
-- Create "ssh_keys" table
|
||||||
|
CREATE TABLE "public"."ssh_keys" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"public_key" text NOT NULL,
|
||||||
|
"encrypted_private_key" text NOT NULL,
|
||||||
|
"private_iv" text NOT NULL,
|
||||||
|
"private_tag" text NOT NULL,
|
||||||
|
"fingerprint" text NOT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_ssh_keys_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_ssh_keys_fingerprint" to table: "ssh_keys"
|
||||||
|
CREATE INDEX "idx_ssh_keys_fingerprint" ON "public"."ssh_keys" ("fingerprint");
|
||||||
|
-- Create index "idx_ssh_keys_organization_id" to table: "ssh_keys"
|
||||||
|
CREATE INDEX "idx_ssh_keys_organization_id" ON "public"."ssh_keys" ("organization_id");
|
||||||
|
-- Create "servers" table
|
||||||
|
CREATE TABLE "public"."servers" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"hostname" text NULL,
|
||||||
|
"public_ip_address" text NULL,
|
||||||
|
"private_ip_address" text NOT NULL,
|
||||||
|
"ssh_user" text NOT NULL,
|
||||||
|
"ssh_key_id" uuid NOT NULL,
|
||||||
|
"role" text NOT NULL,
|
||||||
|
"status" text NULL DEFAULT 'pending',
|
||||||
|
"ssh_host_key" text NULL,
|
||||||
|
"ssh_host_key_algo" text NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_servers_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_servers_ssh_key" FOREIGN KEY ("ssh_key_id") REFERENCES "public"."ssh_keys" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
-- Create "domains" table
|
||||||
|
CREATE TABLE "public"."domains" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"domain_name" character varying(253) NOT NULL,
|
||||||
|
"zone_id" character varying(128) NOT NULL DEFAULT '',
|
||||||
|
"status" character varying(20) NOT NULL DEFAULT 'pending',
|
||||||
|
"last_error" text NOT NULL DEFAULT '',
|
||||||
|
"credential_id" uuid NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_domains_credential" FOREIGN KEY ("credential_id") REFERENCES "public"."credentials" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_domains_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_domains_organization_id" to table: "domains"
|
||||||
|
CREATE INDEX "idx_domains_organization_id" ON "public"."domains" ("organization_id");
|
||||||
|
-- Create index "uniq_org_domain" to table: "domains"
|
||||||
|
CREATE UNIQUE INDEX "uniq_org_domain" ON "public"."domains" ("organization_id", "domain_name");
|
||||||
|
-- Create "record_sets" table
|
||||||
|
CREATE TABLE "public"."record_sets" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"domain_id" uuid NOT NULL,
|
||||||
|
"name" character varying(253) NOT NULL,
|
||||||
|
"type" character varying(10) NOT NULL,
|
||||||
|
"ttl" bigint NULL,
|
||||||
|
"values" jsonb NOT NULL DEFAULT '[]',
|
||||||
|
"fingerprint" character(64) NOT NULL,
|
||||||
|
"status" character varying(20) NOT NULL DEFAULT 'pending',
|
||||||
|
"owner" character varying(16) NOT NULL DEFAULT 'unknown',
|
||||||
|
"last_error" text NOT NULL DEFAULT '',
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_record_sets_domain" FOREIGN KEY ("domain_id") REFERENCES "public"."domains" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_record_sets_domain_id" to table: "record_sets"
|
||||||
|
CREATE INDEX "idx_record_sets_domain_id" ON "public"."record_sets" ("domain_id");
|
||||||
|
-- Create index "idx_record_sets_fingerprint" to table: "record_sets"
|
||||||
|
CREATE INDEX "idx_record_sets_fingerprint" ON "public"."record_sets" ("fingerprint");
|
||||||
|
-- Create index "idx_record_sets_type" to table: "record_sets"
|
||||||
|
CREATE INDEX "idx_record_sets_type" ON "public"."record_sets" ("type");
|
||||||
|
-- Create "clusters" table
|
||||||
|
CREATE TABLE "public"."clusters" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"provider" text NULL,
|
||||||
|
"region" text NULL,
|
||||||
|
"status" character varying(20) NOT NULL DEFAULT 'pre_pending',
|
||||||
|
"last_error" text NOT NULL DEFAULT '',
|
||||||
|
"captain_domain_id" uuid NULL,
|
||||||
|
"control_plane_record_set_id" uuid NULL,
|
||||||
|
"apps_load_balancer_id" uuid NULL,
|
||||||
|
"glue_ops_load_balancer_id" uuid NULL,
|
||||||
|
"bastion_server_id" uuid NULL,
|
||||||
|
"random_token" text NULL,
|
||||||
|
"certificate_key" text NULL,
|
||||||
|
"encrypted_kubeconfig" text NULL,
|
||||||
|
"kube_iv" text NULL,
|
||||||
|
"kube_tag" text NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_clusters_apps_load_balancer" FOREIGN KEY ("apps_load_balancer_id") REFERENCES "public"."load_balancers" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_clusters_bastion_server" FOREIGN KEY ("bastion_server_id") REFERENCES "public"."servers" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_clusters_captain_domain" FOREIGN KEY ("captain_domain_id") REFERENCES "public"."domains" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_clusters_control_plane_record_set" FOREIGN KEY ("control_plane_record_set_id") REFERENCES "public"."record_sets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_clusters_glue_ops_load_balancer" FOREIGN KEY ("glue_ops_load_balancer_id") REFERENCES "public"."load_balancers" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT "fk_clusters_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "node_pools" table
|
||||||
|
CREATE TABLE "public"."node_pools" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"role" text NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_node_pools_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_node_pools_organization_id" to table: "node_pools"
|
||||||
|
CREATE INDEX "idx_node_pools_organization_id" ON "public"."node_pools" ("organization_id");
|
||||||
|
-- Create "cluster_node_pools" table
|
||||||
|
CREATE TABLE "public"."cluster_node_pools" (
|
||||||
|
"node_pool_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"cluster_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
PRIMARY KEY ("node_pool_id", "cluster_id"),
|
||||||
|
CONSTRAINT "fk_cluster_node_pools_cluster" FOREIGN KEY ("cluster_id") REFERENCES "public"."clusters" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_cluster_node_pools_node_pool" FOREIGN KEY ("node_pool_id") REFERENCES "public"."node_pools" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "labels" table
|
||||||
|
CREATE TABLE "public"."labels" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_labels_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create index "idx_labels_organization_id" to table: "labels"
|
||||||
|
CREATE INDEX "idx_labels_organization_id" ON "public"."labels" ("organization_id");
|
||||||
|
-- Create "memberships" table
|
||||||
|
CREATE TABLE "public"."memberships" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"role" text NOT NULL DEFAULT 'member',
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_memberships_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_memberships_user" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
-- Create index "idx_memberships_organization_id" to table: "memberships"
|
||||||
|
CREATE INDEX "idx_memberships_organization_id" ON "public"."memberships" ("organization_id");
|
||||||
|
-- Create index "idx_memberships_user_id" to table: "memberships"
|
||||||
|
CREATE INDEX "idx_memberships_user_id" ON "public"."memberships" ("user_id");
|
||||||
|
-- Create "node_annotations" table
|
||||||
|
CREATE TABLE "public"."node_annotations" (
|
||||||
|
"node_pool_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"annotation_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
PRIMARY KEY ("node_pool_id", "annotation_id"),
|
||||||
|
CONSTRAINT "fk_node_annotations_annotation" FOREIGN KEY ("annotation_id") REFERENCES "public"."annotations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_node_annotations_node_pool" FOREIGN KEY ("node_pool_id") REFERENCES "public"."node_pools" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "node_labels" table
|
||||||
|
CREATE TABLE "public"."node_labels" (
|
||||||
|
"node_pool_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"label_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
PRIMARY KEY ("node_pool_id", "label_id"),
|
||||||
|
CONSTRAINT "fk_node_labels_label" FOREIGN KEY ("label_id") REFERENCES "public"."labels" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_node_labels_node_pool" FOREIGN KEY ("node_pool_id") REFERENCES "public"."node_pools" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "node_servers" table
|
||||||
|
CREATE TABLE "public"."node_servers" (
|
||||||
|
"server_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"node_pool_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
PRIMARY KEY ("server_id", "node_pool_id"),
|
||||||
|
CONSTRAINT "fk_node_servers_node_pool" FOREIGN KEY ("node_pool_id") REFERENCES "public"."node_pools" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_node_servers_server" FOREIGN KEY ("server_id") REFERENCES "public"."servers" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "taints" table
|
||||||
|
CREATE TABLE "public"."taints" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
"effect" text NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_taints_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "node_taints" table
|
||||||
|
CREATE TABLE "public"."node_taints" (
|
||||||
|
"taint_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"node_pool_id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
PRIMARY KEY ("taint_id", "node_pool_id"),
|
||||||
|
CONSTRAINT "fk_node_taints_node_pool" FOREIGN KEY ("node_pool_id") REFERENCES "public"."node_pools" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_node_taints_taint" FOREIGN KEY ("taint_id") REFERENCES "public"."taints" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "master_keys" table
|
||||||
|
CREATE TABLE "public"."master_keys" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"is_active" boolean NULL DEFAULT true,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- Create "organization_keys" table
|
||||||
|
CREATE TABLE "public"."organization_keys" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"master_key_id" uuid NOT NULL,
|
||||||
|
"encrypted_key" text NOT NULL,
|
||||||
|
"iv" text NOT NULL,
|
||||||
|
"tag" text NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_organization_keys_master_key" FOREIGN KEY ("master_key_id") REFERENCES "public"."master_keys" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "fk_organization_keys_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
-- Create "user_emails" table
|
||||||
|
CREATE TABLE "public"."user_emails" (
|
||||||
|
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"is_verified" boolean NOT NULL DEFAULT false,
|
||||||
|
"is_primary" boolean NOT NULL DEFAULT false,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "fk_user_emails_user" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
-- Create index "idx_user_emails_user_id" to table: "user_emails"
|
||||||
|
CREATE INDEX "idx_user_emails_user_id" ON "public"."user_emails" ("user_id");
|
||||||
4
sdk/ts/.gitignore
vendored
Normal file
4
sdk/ts/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
wwwroot/*.js
|
||||||
|
node_modules
|
||||||
|
typings
|
||||||
|
dist
|
||||||
1
sdk/ts/.npmignore
Normal file
1
sdk/ts/.npmignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
README.md
|
||||||
23
sdk/ts/.openapi-generator-ignore
Normal file
23
sdk/ts/.openapi-generator-ignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# OpenAPI Generator Ignore
|
||||||
|
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||||
|
|
||||||
|
# Use this file to prevent files from being overwritten by the generator.
|
||||||
|
# The patterns follow closely to .gitignore or .dockerignore.
|
||||||
|
|
||||||
|
# As an example, the C# client generator defines ApiClient.cs.
|
||||||
|
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||||
|
#ApiClient.cs
|
||||||
|
|
||||||
|
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||||
|
#foo/*/qux
|
||||||
|
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||||
|
#foo/**/qux
|
||||||
|
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can also negate patterns with an exclamation (!).
|
||||||
|
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||||
|
#docs/*.md
|
||||||
|
# Then explicitly reverse the ignore rule for a single file:
|
||||||
|
#!docs/README.md
|
||||||
189
sdk/ts/.openapi-generator/FILES
Normal file
189
sdk/ts/.openapi-generator/FILES
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
.gitignore
|
||||||
|
.npmignore
|
||||||
|
.openapi-generator-ignore
|
||||||
|
README.md
|
||||||
|
docs/AnnotationsApi.md
|
||||||
|
docs/ArcherAdminApi.md
|
||||||
|
docs/AuthApi.md
|
||||||
|
docs/ClustersApi.md
|
||||||
|
docs/CredentialsApi.md
|
||||||
|
docs/DNSApi.md
|
||||||
|
docs/DtoAnnotationResponse.md
|
||||||
|
docs/DtoAttachAnnotationsRequest.md
|
||||||
|
docs/DtoAttachBastionRequest.md
|
||||||
|
docs/DtoAttachCaptainDomainRequest.md
|
||||||
|
docs/DtoAttachLabelsRequest.md
|
||||||
|
docs/DtoAttachLoadBalancerRequest.md
|
||||||
|
docs/DtoAttachNodePoolRequest.md
|
||||||
|
docs/DtoAttachRecordSetRequest.md
|
||||||
|
docs/DtoAttachServersRequest.md
|
||||||
|
docs/DtoAttachTaintsRequest.md
|
||||||
|
docs/DtoAuthStartResponse.md
|
||||||
|
docs/DtoClusterResponse.md
|
||||||
|
docs/DtoCreateAnnotationRequest.md
|
||||||
|
docs/DtoCreateClusterRequest.md
|
||||||
|
docs/DtoCreateCredentialRequest.md
|
||||||
|
docs/DtoCreateDomainRequest.md
|
||||||
|
docs/DtoCreateLabelRequest.md
|
||||||
|
docs/DtoCreateLoadBalancerRequest.md
|
||||||
|
docs/DtoCreateNodePoolRequest.md
|
||||||
|
docs/DtoCreateRecordSetRequest.md
|
||||||
|
docs/DtoCreateSSHRequest.md
|
||||||
|
docs/DtoCreateServerRequest.md
|
||||||
|
docs/DtoCreateTaintRequest.md
|
||||||
|
docs/DtoCredentialOut.md
|
||||||
|
docs/DtoDomainResponse.md
|
||||||
|
docs/DtoEnqueueRequest.md
|
||||||
|
docs/DtoJWK.md
|
||||||
|
docs/DtoJWKS.md
|
||||||
|
docs/DtoJob.md
|
||||||
|
docs/DtoJobStatus.md
|
||||||
|
docs/DtoLabelResponse.md
|
||||||
|
docs/DtoLoadBalancerResponse.md
|
||||||
|
docs/DtoLogoutRequest.md
|
||||||
|
docs/DtoNodePoolResponse.md
|
||||||
|
docs/DtoPageJob.md
|
||||||
|
docs/DtoQueueInfo.md
|
||||||
|
docs/DtoRecordSetResponse.md
|
||||||
|
docs/DtoRefreshRequest.md
|
||||||
|
docs/DtoServerResponse.md
|
||||||
|
docs/DtoSetKubeconfigRequest.md
|
||||||
|
docs/DtoSshResponse.md
|
||||||
|
docs/DtoSshRevealResponse.md
|
||||||
|
docs/DtoTaintResponse.md
|
||||||
|
docs/DtoTokenPair.md
|
||||||
|
docs/DtoUpdateAnnotationRequest.md
|
||||||
|
docs/DtoUpdateClusterRequest.md
|
||||||
|
docs/DtoUpdateCredentialRequest.md
|
||||||
|
docs/DtoUpdateDomainRequest.md
|
||||||
|
docs/DtoUpdateLabelRequest.md
|
||||||
|
docs/DtoUpdateLoadBalancerRequest.md
|
||||||
|
docs/DtoUpdateNodePoolRequest.md
|
||||||
|
docs/DtoUpdateRecordSetRequest.md
|
||||||
|
docs/DtoUpdateServerRequest.md
|
||||||
|
docs/DtoUpdateTaintRequest.md
|
||||||
|
docs/GetSSHKey200Response.md
|
||||||
|
docs/HandlersCreateUserKeyRequest.md
|
||||||
|
docs/HandlersHealthStatus.md
|
||||||
|
docs/HandlersMeResponse.md
|
||||||
|
docs/HandlersMemberOut.md
|
||||||
|
docs/HandlersMemberUpsertReq.md
|
||||||
|
docs/HandlersOrgCreateReq.md
|
||||||
|
docs/HandlersOrgKeyCreateReq.md
|
||||||
|
docs/HandlersOrgKeyCreateResp.md
|
||||||
|
docs/HandlersOrgUpdateReq.md
|
||||||
|
docs/HandlersUpdateMeRequest.md
|
||||||
|
docs/HandlersUserAPIKeyOut.md
|
||||||
|
docs/HandlersVersionResponse.md
|
||||||
|
docs/HealthApi.md
|
||||||
|
docs/LabelsApi.md
|
||||||
|
docs/LoadBalancersApi.md
|
||||||
|
docs/MeAPIKeysApi.md
|
||||||
|
docs/MeApi.md
|
||||||
|
docs/MetaApi.md
|
||||||
|
docs/ModelsAPIKey.md
|
||||||
|
docs/ModelsOrganization.md
|
||||||
|
docs/ModelsUser.md
|
||||||
|
docs/ModelsUserEmail.md
|
||||||
|
docs/NodePoolsApi.md
|
||||||
|
docs/OrgsApi.md
|
||||||
|
docs/ServersApi.md
|
||||||
|
docs/SshApi.md
|
||||||
|
docs/TaintsApi.md
|
||||||
|
docs/UtilsErrorResponse.md
|
||||||
|
package.json
|
||||||
|
src/apis/AnnotationsApi.ts
|
||||||
|
src/apis/ArcherAdminApi.ts
|
||||||
|
src/apis/AuthApi.ts
|
||||||
|
src/apis/ClustersApi.ts
|
||||||
|
src/apis/CredentialsApi.ts
|
||||||
|
src/apis/DNSApi.ts
|
||||||
|
src/apis/HealthApi.ts
|
||||||
|
src/apis/LabelsApi.ts
|
||||||
|
src/apis/LoadBalancersApi.ts
|
||||||
|
src/apis/MeAPIKeysApi.ts
|
||||||
|
src/apis/MeApi.ts
|
||||||
|
src/apis/MetaApi.ts
|
||||||
|
src/apis/NodePoolsApi.ts
|
||||||
|
src/apis/OrgsApi.ts
|
||||||
|
src/apis/ServersApi.ts
|
||||||
|
src/apis/SshApi.ts
|
||||||
|
src/apis/TaintsApi.ts
|
||||||
|
src/apis/index.ts
|
||||||
|
src/index.ts
|
||||||
|
src/models/DtoAnnotationResponse.ts
|
||||||
|
src/models/DtoAttachAnnotationsRequest.ts
|
||||||
|
src/models/DtoAttachBastionRequest.ts
|
||||||
|
src/models/DtoAttachCaptainDomainRequest.ts
|
||||||
|
src/models/DtoAttachLabelsRequest.ts
|
||||||
|
src/models/DtoAttachLoadBalancerRequest.ts
|
||||||
|
src/models/DtoAttachNodePoolRequest.ts
|
||||||
|
src/models/DtoAttachRecordSetRequest.ts
|
||||||
|
src/models/DtoAttachServersRequest.ts
|
||||||
|
src/models/DtoAttachTaintsRequest.ts
|
||||||
|
src/models/DtoAuthStartResponse.ts
|
||||||
|
src/models/DtoClusterResponse.ts
|
||||||
|
src/models/DtoCreateAnnotationRequest.ts
|
||||||
|
src/models/DtoCreateClusterRequest.ts
|
||||||
|
src/models/DtoCreateCredentialRequest.ts
|
||||||
|
src/models/DtoCreateDomainRequest.ts
|
||||||
|
src/models/DtoCreateLabelRequest.ts
|
||||||
|
src/models/DtoCreateLoadBalancerRequest.ts
|
||||||
|
src/models/DtoCreateNodePoolRequest.ts
|
||||||
|
src/models/DtoCreateRecordSetRequest.ts
|
||||||
|
src/models/DtoCreateSSHRequest.ts
|
||||||
|
src/models/DtoCreateServerRequest.ts
|
||||||
|
src/models/DtoCreateTaintRequest.ts
|
||||||
|
src/models/DtoCredentialOut.ts
|
||||||
|
src/models/DtoDomainResponse.ts
|
||||||
|
src/models/DtoEnqueueRequest.ts
|
||||||
|
src/models/DtoJWK.ts
|
||||||
|
src/models/DtoJWKS.ts
|
||||||
|
src/models/DtoJob.ts
|
||||||
|
src/models/DtoJobStatus.ts
|
||||||
|
src/models/DtoLabelResponse.ts
|
||||||
|
src/models/DtoLoadBalancerResponse.ts
|
||||||
|
src/models/DtoLogoutRequest.ts
|
||||||
|
src/models/DtoNodePoolResponse.ts
|
||||||
|
src/models/DtoPageJob.ts
|
||||||
|
src/models/DtoQueueInfo.ts
|
||||||
|
src/models/DtoRecordSetResponse.ts
|
||||||
|
src/models/DtoRefreshRequest.ts
|
||||||
|
src/models/DtoServerResponse.ts
|
||||||
|
src/models/DtoSetKubeconfigRequest.ts
|
||||||
|
src/models/DtoSshResponse.ts
|
||||||
|
src/models/DtoSshRevealResponse.ts
|
||||||
|
src/models/DtoTaintResponse.ts
|
||||||
|
src/models/DtoTokenPair.ts
|
||||||
|
src/models/DtoUpdateAnnotationRequest.ts
|
||||||
|
src/models/DtoUpdateClusterRequest.ts
|
||||||
|
src/models/DtoUpdateCredentialRequest.ts
|
||||||
|
src/models/DtoUpdateDomainRequest.ts
|
||||||
|
src/models/DtoUpdateLabelRequest.ts
|
||||||
|
src/models/DtoUpdateLoadBalancerRequest.ts
|
||||||
|
src/models/DtoUpdateNodePoolRequest.ts
|
||||||
|
src/models/DtoUpdateRecordSetRequest.ts
|
||||||
|
src/models/DtoUpdateServerRequest.ts
|
||||||
|
src/models/DtoUpdateTaintRequest.ts
|
||||||
|
src/models/GetSSHKey200Response.ts
|
||||||
|
src/models/HandlersCreateUserKeyRequest.ts
|
||||||
|
src/models/HandlersHealthStatus.ts
|
||||||
|
src/models/HandlersMeResponse.ts
|
||||||
|
src/models/HandlersMemberOut.ts
|
||||||
|
src/models/HandlersMemberUpsertReq.ts
|
||||||
|
src/models/HandlersOrgCreateReq.ts
|
||||||
|
src/models/HandlersOrgKeyCreateReq.ts
|
||||||
|
src/models/HandlersOrgKeyCreateResp.ts
|
||||||
|
src/models/HandlersOrgUpdateReq.ts
|
||||||
|
src/models/HandlersUpdateMeRequest.ts
|
||||||
|
src/models/HandlersUserAPIKeyOut.ts
|
||||||
|
src/models/HandlersVersionResponse.ts
|
||||||
|
src/models/ModelsAPIKey.ts
|
||||||
|
src/models/ModelsOrganization.ts
|
||||||
|
src/models/ModelsUser.ts
|
||||||
|
src/models/ModelsUserEmail.ts
|
||||||
|
src/models/UtilsErrorResponse.ts
|
||||||
|
src/models/index.ts
|
||||||
|
src/runtime.ts
|
||||||
|
tsconfig.esm.json
|
||||||
|
tsconfig.json
|
||||||
1
sdk/ts/.openapi-generator/VERSION
Normal file
1
sdk/ts/.openapi-generator/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7.17.0
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user