mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-14 05:10:05 +01:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f0edf1007 | ||
|
|
56f86a11b4 | ||
|
|
c9fe259a3a | ||
|
|
d163a050d8 | ||
|
|
9853d32b04 | ||
|
|
d0c43df71c | ||
|
|
219ce80e5b | ||
|
|
7985b310c5 | ||
|
|
501785d471 | ||
|
|
3ca32e9ed7 | ||
|
|
b6e5d329a5 | ||
|
|
c0821253ca | ||
|
|
bb8b1f2773 | ||
|
|
33b0dffba7 | ||
|
|
04cc6facaa | ||
|
|
e414204ac9 | ||
|
|
2975baafb9 | ||
|
|
5819d69d3e | ||
|
|
d0ab259047 | ||
|
|
058b07993c | ||
|
|
92fbf004c6 | ||
|
|
1d89bc4312 | ||
|
|
6626565a75 |
2
.github/workflows/docker-publish.yml
vendored
2
.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
|
||||||
|
|||||||
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
|
||||||
16
Makefile
16
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -134,7 +134,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,
|
||||||
|
|||||||
18
docs/docs.go
18
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
|
||||||
|
|||||||
12
docs/openapi.json
Normal file
12
docs/openapi.json
Normal file
File diff suppressed because one or more lines are too long
6702
docs/openapi.yaml
Normal file
6702
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
78
go.mod
78
go.mod
@@ -3,14 +3,16 @@ module github.com/glueops/autoglue
|
|||||||
go 1.25.4
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
ariga.io/atlas-provider-gorm v0.6.0
|
||||||
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.39.6
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.18
|
github.com/aws/aws-sdk-go-v2/config v1.31.20
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.24
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0
|
github.com/coreos/go-oidc/v3 v3.16.0
|
||||||
github.com/dyaksa/archer v1.1.3
|
github.com/dyaksa/archer v1.1.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
|
||||||
@@ -23,7 +25,6 @@ require (
|
|||||||
github.com/sosedoff/pgweb v0.16.2
|
github.com/sosedoff/pgweb v0.16.2
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
|
||||||
github.com/swaggo/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.44.0
|
||||||
golang.org/x/oauth2 v0.33.0
|
golang.org/x/oauth2 v0.33.0
|
||||||
@@ -34,8 +35,20 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
ariga.io/atlas v0.36.2-0.20250806044935-5bb51a0a956e // indirect
|
||||||
|
cel.dev/expr v0.24.0 // indirect
|
||||||
|
cloud.google.com/go v0.121.6 // indirect
|
||||||
|
cloud.google.com/go/auth v0.16.4 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||||
|
cloud.google.com/go/iam v1.5.2 // indirect
|
||||||
|
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||||
|
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||||
|
cloud.google.com/go/spanner v1.84.1 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
|
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.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.3 // indirect
|
||||||
@@ -48,20 +61,26 @@ require (
|
|||||||
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.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
github.com/aws/smithy-go v1.23.2 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/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/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // 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/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/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-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
github.com/go-openapi/spec v0.20.9 // indirect
|
github.com/go-openapi/spec v0.20.9 // indirect
|
||||||
@@ -72,6 +91,15 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
|
github.com/googleapis/go-gorm-spanner v1.8.6 // indirect
|
||||||
|
github.com/googleapis/go-sql-spanner v1.17.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
@@ -89,14 +117,17 @@ 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/microsoft/go-mssqldb v1.7.2 // 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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // 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
|
||||||
@@ -107,14 +138,25 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.5.0 // 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/errs v1.4.0 // indirect
|
||||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||||
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 // 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
|
||||||
@@ -123,8 +165,16 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/api v0.247.0 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||||
|
google.golang.org/grpc v1.74.2 // 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
|
||||||
|
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||||
|
gorm.io/driver/sqlserver v1.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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, authOrg)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
41
internal/api/mount_cluster_routes.go
Normal file
41
internal/api/mount_cluster_routes.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountClusterRoutes(r chi.Router, db *gorm.DB, 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.DeleteNodePool(db))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
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,19 +104,20 @@ 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
|
||||||
}
|
} else {
|
||||||
|
|
||||||
fmt.Println("Running in production mode")
|
fmt.Println("Running in production mode")
|
||||||
if h, err := web.SPAHandler(); err == nil {
|
if h, err := web.SPAHandler(); err == nil {
|
||||||
r.NotFound(h.ServeHTTP)
|
r.NotFound(h.ServeHTTP)
|
||||||
} else {
|
} else {
|
||||||
log.Error().Err(err).Msg("spa handler init failed")
|
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,11 @@ 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{},
|
||||||
)
|
)
|
||||||
|
|
||||||
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{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePostMessageHTML sends a tiny HTML page that posts tokens to the SPA and closes the window.
|
// postMessage HTML template
|
||||||
func writePostMessageHTML(w http.ResponseWriter, origin string, payload dto.TokenPair) {
|
var postMessageTpl = template.Must(template.New("postmsg").Parse(`<!doctype html>
|
||||||
b, _ := json.Marshal(payload)
|
<html>
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
<body>
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
<script>
|
||||||
w.WriteHeader(http.StatusOK)
|
(function(){
|
||||||
_, _ = w.Write([]byte(`<!doctype html><html><body><script>
|
|
||||||
(function(){
|
|
||||||
try {
|
try {
|
||||||
var data = ` + string(b) + `;
|
var data = JSON.parse(atob("{{.PayloadB64}}"));
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
window.opener.postMessage({ type: 'autoglue:auth', payload: data }, '` + origin + `');
|
window.opener.postMessage(
|
||||||
|
{ type: 'autoglue:auth', payload: data },
|
||||||
|
"{{.Origin}}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
window.close();
|
window.close();
|
||||||
})();
|
})();
|
||||||
</script></body></html>`))
|
</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.
|
||||||
|
func writePostMessageHTML(w http.ResponseWriter, origin string, payload dto.TokenPair) {
|
||||||
|
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("Cache-Control", "no-store")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = postMessageTpl.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalOrigin returns scheme://host[:port] for a given URL, or "" if invalid.
|
||||||
|
func canonicalOrigin(raw string) string {
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,11 +23,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ListCredentials godoc
|
// ListCredentials godoc
|
||||||
|
//
|
||||||
// @ID ListCredentials
|
// @ID ListCredentials
|
||||||
// @Summary List credentials (metadata only)
|
// @Summary List credentials (metadata only)
|
||||||
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
||||||
// @Tags Credentials
|
// @Tags Credentials
|
||||||
// @Accept json
|
|
||||||
// @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 provider query string false "Filter by provider (e.g., aws)"
|
||||||
@@ -73,10 +73,10 @@ func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCredential godoc
|
// GetCredential godoc
|
||||||
|
//
|
||||||
// @ID GetCredential
|
// @ID GetCredential
|
||||||
// @Summary Get credential by ID (metadata only)
|
// @Summary Get credential by ID (metadata only)
|
||||||
// @Tags Credentials
|
// @Tags Credentials
|
||||||
// @Accept json
|
|
||||||
// @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)"
|
||||||
@@ -117,6 +117,7 @@ func GetCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateCredential godoc
|
// CreateCredential godoc
|
||||||
|
//
|
||||||
// @ID CreateCredential
|
// @ID CreateCredential
|
||||||
// @Summary Create a credential (encrypts secret)
|
// @Summary Create a credential (encrypts secret)
|
||||||
// @Tags Credentials
|
// @Tags Credentials
|
||||||
@@ -166,6 +167,7 @@ func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCredential godoc
|
// UpdateCredential godoc
|
||||||
|
//
|
||||||
// @ID UpdateCredential
|
// @ID UpdateCredential
|
||||||
// @Summary Update credential metadata and/or rotate secret
|
// @Summary Update credential metadata and/or rotate secret
|
||||||
// @Tags Credentials
|
// @Tags Credentials
|
||||||
@@ -296,10 +298,10 @@ func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCredential godoc
|
// DeleteCredential godoc
|
||||||
|
//
|
||||||
// @ID DeleteCredential
|
// @ID DeleteCredential
|
||||||
// @Summary Delete credential
|
// @Summary Delete credential
|
||||||
// @Tags Credentials
|
// @Tags Credentials
|
||||||
// @Accept json
|
|
||||||
// @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)"
|
||||||
@@ -335,6 +337,7 @@ func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RevealCredential godoc
|
// RevealCredential godoc
|
||||||
|
//
|
||||||
// @ID RevealCredential
|
// @ID RevealCredential
|
||||||
// @Summary Reveal decrypted secret (one-time read)
|
// @Summary Reveal decrypted secret (one-time read)
|
||||||
// @Tags Credentials
|
// @Tags Credentials
|
||||||
|
|||||||
@@ -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)"
|
||||||
@@ -216,7 +215,6 @@ func ListDomains(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @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)"
|
||||||
@@ -393,7 +391,6 @@ func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @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)"
|
||||||
@@ -437,7 +434,6 @@ 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)"
|
||||||
@@ -723,7 +719,6 @@ func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
|||||||
// @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)"
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ 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"`
|
||||||
|
CaptainDomain *DomainResponse `json:"captain_domain,omitempty"`
|
||||||
|
ControlPlaneRecordSet *RecordSetResponse `json:"control_plane_record_set,omitempty"`
|
||||||
|
AppsLoadBalancer *LoadBalancerResponse `json:"apps_load_balancer,omitempty"`
|
||||||
|
GlueOpsLoadBalancer *LoadBalancerResponse `json:"glueops_load_balancer,omitempty"`
|
||||||
|
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CaptainDomain string `json:"captain_domain"`
|
LastError string `json:"last_error"`
|
||||||
ClusterLoadBalancer string `json:"cluster_load_balancer"`
|
|
||||||
RandomToken string `json:"random_token"`
|
RandomToken string `json:"random_token"`
|
||||||
CertificateKey string `json:"certificate_key"`
|
CertificateKey string `json:"certificate_key"`
|
||||||
ControlLoadBalancer string `json:"control_load_balancer"`
|
|
||||||
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
||||||
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -27,8 +29,34 @@ type CreateClusterRequest struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Status string `json:"status"`
|
}
|
||||||
CaptainDomain string `json:"captain_domain"`
|
|
||||||
ClusterLoadBalancer *string `json:"cluster_load_balancer"`
|
type UpdateClusterRequest struct {
|
||||||
ControlLoadBalancer *string `json:"control_load_balancer"`
|
Name *string `json:"name,omitempty"`
|
||||||
|
Provider *string `json:"provider,omitempty"`
|
||||||
|
Region *string `json:"region,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"`
|
||||||
}
|
}
|
||||||
|
|||||||
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,7 +25,6 @@ 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)"
|
||||||
@@ -145,7 +144,6 @@ 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)"
|
||||||
@@ -327,11 +325,10 @@ 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"
|
||||||
@@ -369,7 +366,6 @@ 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)"
|
||||||
@@ -521,7 +517,6 @@ 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)"
|
||||||
@@ -588,7 +583,6 @@ 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)"
|
||||||
@@ -730,7 +724,6 @@ 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)"
|
||||||
@@ -798,7 +791,6 @@ 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)"
|
||||||
@@ -940,7 +932,6 @@ 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)"
|
||||||
@@ -1008,7 +999,6 @@ 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)"
|
||||||
@@ -1151,7 +1141,6 @@ 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)"
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ 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
|
||||||
|
)
|
||||||
|
|
||||||
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"`
|
||||||
@@ -13,21 +22,24 @@ type Cluster struct {
|
|||||||
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"`
|
||||||
|
AppsLoadBalancer *LoadBalancer `gorm:"foreignKey:AppsLoadBalancerID" json:"apps_load_balancer,omitempty"`
|
||||||
|
GlueOpsLoadBalancerID *uuid.UUID `gorm:"type:uuid" json:"glueops_load_balancer_id,omitempty"`
|
||||||
|
GlueOpsLoadBalancer *LoadBalancer `gorm:"foreignKey:GlueOpsLoadBalancerID" json:"glueops_load_balancer,omitempty"`
|
||||||
|
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||||
|
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
||||||
|
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||||
RandomToken string `json:"random_token"`
|
RandomToken string `json:"random_token"`
|
||||||
CertificateKey string `json:"certificate_key"`
|
CertificateKey string `json:"certificate_key"`
|
||||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||||
KubeIV string `json:"-"`
|
KubeIV string `json:"-"`
|
||||||
KubeTag string `json:"-"`
|
KubeTag string `json:"-"`
|
||||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
|
||||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
|
||||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
18
main.go
18
main.go
@@ -1,21 +1,22 @@
|
|||||||
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
|
||||||
|
|
||||||
// @BasePath /api/v1
|
// @servers.url https://autoglue.onglueops.rocks/api/v1
|
||||||
// @schemes http https
|
// @servers.description 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
|
||||||
|
|
||||||
// @securityDefinitions.apikey BearerAuth
|
// @securityDefinitions.apikey BearerAuth
|
||||||
// @in header
|
// @in header
|
||||||
@@ -38,7 +39,6 @@ import (
|
|||||||
// @description Org-level 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,5 @@ 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
|
||||||
|
USER non-root
|
||||||
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");
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Target struct {
|
|
||||||
Name string
|
|
||||||
URL string
|
|
||||||
SHA256 string
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = "0.16.2"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
targets := []Target{
|
|
||||||
{
|
|
||||||
Name: "pgweb-linux-amd64",
|
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_amd64.zip", version),
|
|
||||||
SHA256: "3d6c2063e1040b8a625eb7c43c9b84f8ed12cfc9a798eacbce85179963ee2554",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "pgweb-linux-arm64",
|
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_arm64.zip", version),
|
|
||||||
SHA256: "079c698a323ed6431ce7e6343ee5847c7da62afbf45dfb2e78f8289d7b381783",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "pgweb-darwin-amd64",
|
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_amd64.zip", version),
|
|
||||||
SHA256: "c0a098e2eb9cf9f7c20161a2947522eb67eacbf2b6c3389c2f8e8c5ed7238957",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "pgweb-darwin-arm64",
|
|
||||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_arm64.zip", version),
|
|
||||||
SHA256: "c8f5fca847f461ba22a619e2d96cb1656cefdffd8f2aef2340e14fc5b518d3a2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
outDir := filepath.Join("internal", "web", "pgwebbin")
|
|
||||||
_ = os.MkdirAll(outDir, 0o755)
|
|
||||||
|
|
||||||
for _, t := range targets {
|
|
||||||
destZip := filepath.Join(outDir, t.Name+".zip")
|
|
||||||
fmt.Printf("Downloading %s...\n", t.URL)
|
|
||||||
if err := downloadFile(destZip, t.URL); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
binPath := filepath.Join(outDir, t.Name)
|
|
||||||
if err := unzipSingle(destZip, binPath); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
_ = os.Remove(destZip)
|
|
||||||
|
|
||||||
// Make executable
|
|
||||||
if err := os.Chmod(binPath, 0o755); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Saved %s\n", binPath)
|
|
||||||
|
|
||||||
// Compute checksum
|
|
||||||
sum, _ := fileSHA256(binPath)
|
|
||||||
fmt.Printf(" SHA256: %s\n", sum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadFile(dest, url string) error {
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return fmt.Errorf("bad status: %s", resp.Status)
|
|
||||||
}
|
|
||||||
out, err := os.Create(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
_, err = io.Copy(out, resp.Body)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileSHA256(path string) (string, error) {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
h := sha256.New()
|
|
||||||
if _, err := io.Copy(h, f); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unzipSingle(zipPath, outPath string) error {
|
|
||||||
zr, err := zip.OpenReader(zipPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer zr.Close()
|
|
||||||
|
|
||||||
if len(zr.File) == 0 {
|
|
||||||
return fmt.Errorf("zip file %s is empty", zipPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
f := zr.File[0]
|
|
||||||
|
|
||||||
rc, err := f.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
|
|
||||||
out, err := os.Create(outPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(out, rc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,29 @@
|
|||||||
import js from '@eslint/js'
|
import js from "@eslint/js"
|
||||||
import globals from 'globals'
|
import reactHooks from "eslint-plugin-react-hooks"
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactRefresh from "eslint-plugin-react-refresh"
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import { defineConfig, globalIgnores } from "eslint/config"
|
||||||
import tseslint from 'typescript-eslint'
|
import globals from "globals"
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import tseslint from "typescript-eslint"
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(["dist", "src/sdk", "src/components"]),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ["**/*.{ts,tsx}"],
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
reactHooks.configs['recommended-latest'],
|
//reactHooks.configs['recommended-latest'],
|
||||||
reactRefresh.configs.vite,
|
//reactRefresh.configs.vite,
|
||||||
],
|
],
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...(reactHooks.configs["recommended-latest"]?.rules ?? {}),
|
||||||
|
...(reactRefresh.configs.vite?.rules ?? {}),
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
},
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
|
|||||||
@@ -37,8 +37,9 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -46,14 +47,16 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
|
"motion": "^12.23.24",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"rapidoc": "^9.3.8",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.66.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.6",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
@@ -64,10 +67,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.39.1",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
|
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
|
||||||
"@types/node": "24.10.0",
|
"@types/node": "24.10.1",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.5",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vitejs/plugin-react": "5.1.0",
|
"@vitejs/plugin-react": "5.1.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-plugin-react-hooks": "7.0.1",
|
"eslint-plugin-react-hooks": "7.0.1",
|
||||||
"eslint-plugin-react-refresh": "0.4.24",
|
"eslint-plugin-react-refresh": "0.4.24",
|
||||||
|
|||||||
@@ -2,25 +2,30 @@ import { AppShell } from "@/layouts/app-shell.tsx"
|
|||||||
import { Route, Routes } from "react-router-dom"
|
import { Route, Routes } from "react-router-dom"
|
||||||
|
|
||||||
import { ProtectedRoute } from "@/components/protected-route.tsx"
|
import { ProtectedRoute } from "@/components/protected-route.tsx"
|
||||||
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
import { AnnotationPage } from "@/pages/annotation-page.tsx"
|
||||||
import { Login } from "@/pages/auth/login.tsx"
|
import { ClustersPage } from "@/pages/cluster-page"
|
||||||
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
import { CredentialPage } from "@/pages/credential-page.tsx"
|
||||||
import { DnsPage } from "@/pages/dns/dns-page.tsx"
|
import { DnsPage } from "@/pages/dns-page.tsx"
|
||||||
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
import { DocsPage } from "@/pages/docs-page.tsx"
|
||||||
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
import { JobsPage } from "@/pages/jobs-page.tsx"
|
||||||
import { MePage } from "@/pages/me/me-page.tsx"
|
import { LabelsPage } from "@/pages/labels-page.tsx"
|
||||||
import { NodePoolsPage } from "@/pages/nodepools/node-pools-page.tsx"
|
import { LoadBalancersPage } from "@/pages/load-balancers-page"
|
||||||
|
import { Login } from "@/pages/login.tsx"
|
||||||
|
import { MePage } from "@/pages/me-page.tsx"
|
||||||
|
import { NodePoolsPage } from "@/pages/node-pools-page.tsx"
|
||||||
import { OrgApiKeys } from "@/pages/org/api-keys.tsx"
|
import { OrgApiKeys } from "@/pages/org/api-keys.tsx"
|
||||||
import { OrgMembers } from "@/pages/org/members.tsx"
|
import { OrgMembers } from "@/pages/org/members.tsx"
|
||||||
import { OrgSettings } from "@/pages/org/settings.tsx"
|
import { OrgSettings } from "@/pages/org/settings.tsx"
|
||||||
import { ServerPage } from "@/pages/servers/server-page.tsx"
|
import { ServerPage } from "@/pages/server-page.tsx"
|
||||||
import { SshPage } from "@/pages/ssh/ssh-page.tsx"
|
import { SshPage } from "@/pages/ssh-page.tsx"
|
||||||
import { TaintsPage } from "@/pages/taints/taints-page.tsx"
|
import { TaintsPage } from "@/pages/taints-page.tsx"
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/docs" element={<DocsPage />} />
|
||||||
|
|
||||||
<Route element={<ProtectedRoute />}>
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route element={<AppShell />}>
|
<Route element={<AppShell />}>
|
||||||
<Route path="/me" element={<MePage />} />
|
<Route path="/me" element={<MePage />} />
|
||||||
@@ -37,6 +42,8 @@ export default function App() {
|
|||||||
<Route path="/node-pools" element={<NodePoolsPage />} />
|
<Route path="/node-pools" element={<NodePoolsPage />} />
|
||||||
<Route path="/credentials" element={<CredentialPage />} />
|
<Route path="/credentials" element={<CredentialPage />} />
|
||||||
<Route path="/dns" element={<DnsPage />} />
|
<Route path="/dns" element={<DnsPage />} />
|
||||||
|
<Route path="/load-balancers" element={<LoadBalancersPage />} />
|
||||||
|
<Route path="/clusters" element={<ClustersPage />} />
|
||||||
|
|
||||||
<Route path="/admin/jobs" element={<JobsPage />} />
|
<Route path="/admin/jobs" element={<JobsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ export const annotationsApi = {
|
|||||||
}),
|
}),
|
||||||
createAnnotation: (body: DtoCreateAnnotationRequest) =>
|
createAnnotation: (body: DtoCreateAnnotationRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await annotations.createAnnotation({ body })
|
return await annotations.createAnnotation({
|
||||||
|
dtoCreateAnnotationRequest: body,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
getAnnotation: (id: string) =>
|
getAnnotation: (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -22,6 +24,9 @@ export const annotationsApi = {
|
|||||||
}),
|
}),
|
||||||
updateAnnotation: (id: string, body: DtoUpdateAnnotationRequest) =>
|
updateAnnotation: (id: string, body: DtoUpdateAnnotationRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await annotations.updateAnnotation({ id, body })
|
return await annotations.updateAnnotation({
|
||||||
|
id,
|
||||||
|
dtoUpdateAnnotationRequest: body,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,6 @@ import { makeArcherAdminApi } from "@/sdkClient.ts"
|
|||||||
|
|
||||||
const archerAdmin = makeArcherAdminApi()
|
const archerAdmin = makeArcherAdminApi()
|
||||||
|
|
||||||
type ListParams = {
|
|
||||||
status?: "queued" | "running" | "succeeded" | "failed" | "canceled" | "retrying" | "scheduled"
|
|
||||||
queue?: string
|
|
||||||
q?: string
|
|
||||||
page?: number
|
|
||||||
pageSize?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const archerAdminApi = {
|
export const archerAdminApi = {
|
||||||
listJobs: (params: AdminListArcherJobsRequest = {}) => {
|
listJobs: (params: AdminListArcherJobsRequest = {}) => {
|
||||||
return withRefresh(async () => {
|
return withRefresh(async () => {
|
||||||
@@ -25,7 +17,7 @@ export const archerAdminApi = {
|
|||||||
run_at?: string
|
run_at?: string
|
||||||
}) => {
|
}) => {
|
||||||
return withRefresh(async () => {
|
return withRefresh(async () => {
|
||||||
return await archerAdmin.adminEnqueueArcherJob({ body })
|
return await archerAdmin.adminEnqueueArcherJob({ dtoEnqueueRequest: body })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
retryJob: (id: string) => {
|
retryJob: (id: string) => {
|
||||||
|
|||||||
150
ui/src/api/clusters.ts
Normal file
150
ui/src/api/clusters.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { withRefresh } from "@/api/with-refresh"
|
||||||
|
import type {
|
||||||
|
DtoAttachBastionRequest,
|
||||||
|
DtoAttachCaptainDomainRequest,
|
||||||
|
DtoAttachLoadBalancerRequest,
|
||||||
|
DtoAttachRecordSetRequest,
|
||||||
|
DtoCreateClusterRequest,
|
||||||
|
DtoSetKubeconfigRequest,
|
||||||
|
DtoUpdateClusterRequest,
|
||||||
|
} from "@/sdk"
|
||||||
|
import { makeClusterApi } from "@/sdkClient"
|
||||||
|
|
||||||
|
const clusters = makeClusterApi()
|
||||||
|
|
||||||
|
export const clustersApi = {
|
||||||
|
// --- basic CRUD ---
|
||||||
|
|
||||||
|
listClusters: (q?: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.listClusters(q ? { q } : {})
|
||||||
|
}),
|
||||||
|
|
||||||
|
getCluster: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.getCluster({ clusterID: id })
|
||||||
|
}),
|
||||||
|
|
||||||
|
createCluster: (body: DtoCreateClusterRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.createCluster({
|
||||||
|
dtoCreateClusterRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateCluster: (id: string, body: DtoUpdateClusterRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.updateCluster({
|
||||||
|
clusterID: id,
|
||||||
|
dtoUpdateClusterRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteCluster: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.deleteCluster({ clusterID: id })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- kubeconfig ---
|
||||||
|
|
||||||
|
setKubeconfig: (clusterID: string, body: DtoSetKubeconfigRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.setClusterKubeconfig({
|
||||||
|
clusterID,
|
||||||
|
dtoSetKubeconfigRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearKubeconfig: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.clearClusterKubeconfig({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- captain domain ---
|
||||||
|
|
||||||
|
attachCaptainDomain: (clusterID: string, body: DtoAttachCaptainDomainRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachCaptainDomain({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachCaptainDomainRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachCaptainDomain: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachCaptainDomain({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- control plane record set ---
|
||||||
|
|
||||||
|
attachControlPlaneRecordSet: (clusterID: string, body: DtoAttachRecordSetRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachControlPlaneRecordSet({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachRecordSetRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachControlPlaneRecordSet: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachControlPlaneRecordSet({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- load balancers ---
|
||||||
|
|
||||||
|
attachAppsLoadBalancer: (clusterID: string, body: DtoAttachLoadBalancerRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachAppsLoadBalancer({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachLoadBalancerRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachAppsLoadBalancer: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachAppsLoadBalancer({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
attachGlueOpsLoadBalancer: (clusterID: string, body: DtoAttachLoadBalancerRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachGlueOpsLoadBalancer({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachLoadBalancerRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachGlueOpsLoadBalancer: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachGlueOpsLoadBalancer({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- bastion ---
|
||||||
|
|
||||||
|
attachBastion: (clusterID: string, body: DtoAttachBastionRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachBastionServer({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachBastionRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachBastion: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachBastionServer({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// -- node-pools
|
||||||
|
|
||||||
|
attachNodePool: (clusterID: string, nodePoolID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachNodePool({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachNodePoolRequest: { node_pool_id: nodePoolID },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachNodePool: (clusterID: string, nodePoolID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachNodePool({ clusterID, nodePoolID })
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export const credentialsApi = {
|
|||||||
}),
|
}),
|
||||||
createCredential: async (body: DtoCreateCredentialRequest) =>
|
createCredential: async (body: DtoCreateCredentialRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await credentials.createCredential({ body })
|
return await credentials.createCredential({ dtoCreateCredentialRequest: body })
|
||||||
}),
|
}),
|
||||||
getCredential: async (id: string) =>
|
getCredential: async (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -23,7 +23,7 @@ export const credentialsApi = {
|
|||||||
}),
|
}),
|
||||||
updateCredential: async (id: string, body: DtoUpdateCredentialRequest) =>
|
updateCredential: async (id: string, body: DtoUpdateCredentialRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await credentials.updateCredential({ id, body })
|
return await credentials.updateCredential({ id, dtoUpdateCredentialRequest: body })
|
||||||
}),
|
}),
|
||||||
revealCredential: async (id: string) =>
|
revealCredential: async (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export const dnsApi = {
|
|||||||
}),
|
}),
|
||||||
createDomain: async (body: DtoCreateDomainRequest) =>
|
createDomain: async (body: DtoCreateDomainRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await dns.createDomain({ body })
|
return await dns.createDomain({ dtoCreateDomainRequest: body })
|
||||||
}),
|
}),
|
||||||
updateDomain: async (id: string, body: DtoUpdateDomainRequest) =>
|
updateDomain: async (id: string, body: DtoUpdateDomainRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await dns.updateDomain({ id, body })
|
return await dns.updateDomain({ id, dtoUpdateDomainRequest: body })
|
||||||
}),
|
}),
|
||||||
deleteDomain: async (id: string) =>
|
deleteDomain: async (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -36,11 +36,11 @@ export const dnsApi = {
|
|||||||
}),
|
}),
|
||||||
createRecordSetsByDomain: async (domainId: string, body: DtoCreateRecordSetRequest) =>
|
createRecordSetsByDomain: async (domainId: string, body: DtoCreateRecordSetRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await dns.createRecordSet({ domainId, body })
|
return await dns.createRecordSet({ domainId, dtoCreateRecordSetRequest: body })
|
||||||
}),
|
}),
|
||||||
updateRecordSetsByDomain: async (id: string, body: DtoUpdateRecordSetRequest) =>
|
updateRecordSetsByDomain: async (id: string, body: DtoUpdateRecordSetRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await dns.updateRecordSet({ id, body })
|
return await dns.updateRecordSet({ id, dtoUpdateRecordSetRequest: body })
|
||||||
}),
|
}),
|
||||||
deleteRecordSetsByDomain: async (id: string) =>
|
deleteRecordSetsByDomain: async (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const labelsApi = {
|
|||||||
}),
|
}),
|
||||||
createLabel: (body: DtoCreateLabelRequest) =>
|
createLabel: (body: DtoCreateLabelRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await labels.createLabel({ body })
|
return await labels.createLabel({ dtoCreateLabelRequest: body })
|
||||||
}),
|
}),
|
||||||
getLabel: (id: string) =>
|
getLabel: (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -23,6 +23,6 @@ export const labelsApi = {
|
|||||||
}),
|
}),
|
||||||
updateLabel: (id: string, body: DtoUpdateLabelRequest) =>
|
updateLabel: (id: string, body: DtoUpdateLabelRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await labels.updateLabel({ id, body })
|
return await labels.updateLabel({ id, dtoUpdateLabelRequest: body })
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
32
ui/src/api/loadbalancers.ts
Normal file
32
ui/src/api/loadbalancers.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { withRefresh } from "@/api/with-refresh"
|
||||||
|
import type { DtoCreateLoadBalancerRequest, DtoUpdateLoadBalancerRequest } from "@/sdk"
|
||||||
|
import { makeLoadBalancerApi } from "@/sdkClient"
|
||||||
|
|
||||||
|
const loadBalancers = makeLoadBalancerApi()
|
||||||
|
export const loadBalancersApi = {
|
||||||
|
listLoadBalancers: () =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await loadBalancers.listLoadBalancers()
|
||||||
|
}),
|
||||||
|
getLoadBalancer: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await loadBalancers.getLoadBalancers({ id })
|
||||||
|
}),
|
||||||
|
createLoadBalancer: (body: DtoCreateLoadBalancerRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await loadBalancers.createLoadBalancer({
|
||||||
|
dtoCreateLoadBalancerRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
updateLoadBalancer: (id: string, body: DtoUpdateLoadBalancerRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await loadBalancers.updateLoadBalancer({
|
||||||
|
id,
|
||||||
|
dtoUpdateLoadBalancerRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
deleteLoadBalancer: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await loadBalancers.deleteLoadBalancer({ id })
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export const meApi = {
|
|||||||
|
|
||||||
updateMe: (body: HandlersUpdateMeRequest) =>
|
updateMe: (body: HandlersUpdateMeRequest) =>
|
||||||
withRefresh(async (): Promise<ModelsUser> => {
|
withRefresh(async (): Promise<ModelsUser> => {
|
||||||
return await me.updateMe({ body })
|
return await me.updateMe({ handlersUpdateMeRequest: body })
|
||||||
}),
|
}),
|
||||||
|
|
||||||
listKeys: () =>
|
listKeys: () =>
|
||||||
@@ -29,7 +29,7 @@ export const meApi = {
|
|||||||
|
|
||||||
createKey: (body: HandlersCreateUserKeyRequest) =>
|
createKey: (body: HandlersCreateUserKeyRequest) =>
|
||||||
withRefresh(async (): Promise<HandlersUserAPIKeyOut> => {
|
withRefresh(async (): Promise<HandlersUserAPIKeyOut> => {
|
||||||
return await keys.createUserAPIKey({ body })
|
return await keys.createUserAPIKey({ handlersCreateUserKeyRequest: body })
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteKey: (id: string) =>
|
deleteKey: (id: string) =>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const nodePoolsApi = {
|
|||||||
}),
|
}),
|
||||||
createNodePool: (body: DtoCreateNodePoolRequest) =>
|
createNodePool: (body: DtoCreateNodePoolRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await nodePools.createNodePool({ body })
|
return await nodePools.createNodePool({ dtoCreateNodePoolRequest: body })
|
||||||
}),
|
}),
|
||||||
getNodePool: (id: string) =>
|
getNodePool: (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -34,7 +34,7 @@ export const nodePoolsApi = {
|
|||||||
}),
|
}),
|
||||||
updateNodePool: (id: string, body: DtoUpdateNodePoolRequest) =>
|
updateNodePool: (id: string, body: DtoUpdateNodePoolRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await nodePools.updateNodePool({ id, body })
|
return await nodePools.updateNodePool({ id, dtoUpdateNodePoolRequest: body })
|
||||||
}),
|
}),
|
||||||
// Servers
|
// Servers
|
||||||
listNodePoolServers: (id: string) =>
|
listNodePoolServers: (id: string) =>
|
||||||
@@ -43,7 +43,7 @@ export const nodePoolsApi = {
|
|||||||
}),
|
}),
|
||||||
attachNodePoolServer: (id: string, body: DtoAttachServersRequest) =>
|
attachNodePoolServer: (id: string, body: DtoAttachServersRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await nodePools.attachNodePoolServers({ id, body })
|
return await nodePools.attachNodePoolServers({ id, dtoAttachServersRequest: body })
|
||||||
}),
|
}),
|
||||||
detachNodePoolServers: (id: string, serverId: string) =>
|
detachNodePoolServers: (id: string, serverId: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -56,7 +56,7 @@ export const nodePoolsApi = {
|
|||||||
}),
|
}),
|
||||||
attachNodePoolTaints: (id: string, body: DtoAttachTaintsRequest) =>
|
attachNodePoolTaints: (id: string, body: DtoAttachTaintsRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await nodePools.attachNodePoolTaints({ id, body })
|
return await nodePools.attachNodePoolTaints({ id, dtoAttachTaintsRequest: body })
|
||||||
}),
|
}),
|
||||||
detachNodePoolTaints: (id: string, taintId: string) =>
|
detachNodePoolTaints: (id: string, taintId: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -69,7 +69,7 @@ export const nodePoolsApi = {
|
|||||||
}),
|
}),
|
||||||
attachNodePoolLabels: (id: string, body: DtoAttachLabelsRequest) =>
|
attachNodePoolLabels: (id: string, body: DtoAttachLabelsRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await nodePools.attachNodePoolLabels({ id, body })
|
return await nodePools.attachNodePoolLabels({ id, dtoAttachLabelsRequest: body })
|
||||||
}),
|
}),
|
||||||
detachNodePoolLabels: (id: string, labelId: string) =>
|
detachNodePoolLabels: (id: string, labelId: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -82,7 +82,7 @@ export const nodePoolsApi = {
|
|||||||
}),
|
}),
|
||||||
attachNodePoolAnnotations: (id: string, body: DtoAttachAnnotationsRequest) =>
|
attachNodePoolAnnotations: (id: string, body: DtoAttachAnnotationsRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await nodePools.attachNodePoolAnnotations({ id, body })
|
return await nodePools.attachNodePoolAnnotations({ id, dtoAttachAnnotationsRequest: body })
|
||||||
}),
|
}),
|
||||||
detachNodePoolAnnotations: (id: string, annotationId: string) =>
|
detachNodePoolAnnotations: (id: string, annotationId: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const serversApi = {
|
|||||||
}),
|
}),
|
||||||
createServer: (body: DtoCreateServerRequest) =>
|
createServer: (body: DtoCreateServerRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await servers.createServer({ body })
|
return await servers.createServer({ dtoCreateServerRequest: body })
|
||||||
}),
|
}),
|
||||||
getServer: (id: string) =>
|
getServer: (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -19,7 +19,7 @@ export const serversApi = {
|
|||||||
}),
|
}),
|
||||||
updateServer: (id: string, body: DtoUpdateServerRequest) =>
|
updateServer: (id: string, body: DtoUpdateServerRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await servers.updateServer({ id, body })
|
return await servers.updateServer({ id, dtoUpdateServerRequest: body })
|
||||||
}),
|
}),
|
||||||
deleteServer: (id: string) =>
|
deleteServer: (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const sshApi = {
|
|||||||
createSshKey: (body: DtoCreateSSHRequest) =>
|
createSshKey: (body: DtoCreateSSHRequest) =>
|
||||||
withRefresh(async (): Promise<DtoSshResponse> => {
|
withRefresh(async (): Promise<DtoSshResponse> => {
|
||||||
// SDK expects { body }
|
// SDK expects { body }
|
||||||
return await ssh.createSSHKey({ body })
|
return await ssh.createSSHKey({ dtoCreateSSHRequest: body })
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getSshKeyById: (id: string) =>
|
getSshKeyById: (id: string) =>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const taintsApi = {
|
|||||||
}),
|
}),
|
||||||
createTaint: (body: DtoCreateTaintRequest) =>
|
createTaint: (body: DtoCreateTaintRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await taints.createTaint({ body })
|
return await taints.createTaint({ dtoCreateTaintRequest: body })
|
||||||
}),
|
}),
|
||||||
getTaint: (id: string) =>
|
getTaint: (id: string) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
@@ -22,6 +22,6 @@ export const taintsApi = {
|
|||||||
}),
|
}),
|
||||||
updateTaint: (id: string, body: DtoUpdateTaintRequest) =>
|
updateTaint: (id: string, body: DtoUpdateTaintRequest) =>
|
||||||
withRefresh(async () => {
|
withRefresh(async () => {
|
||||||
return await taints.updateTaint({ id, body })
|
return await taints.updateTaint({ id, dtoUpdateTaintRequest: body })
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { orgStore } from "@/auth/org.ts"
|
|||||||
import { Footer } from "@/layouts/footer.tsx"
|
import { Footer } from "@/layouts/footer.tsx"
|
||||||
import { adminNav, mainNav, orgNav, userNav } from "@/layouts/nav-config.ts"
|
import { adminNav, mainNav, orgNav, userNav } from "@/layouts/nav-config.ts"
|
||||||
import { OrgSwitcher } from "@/layouts/org-switcher.tsx"
|
import { OrgSwitcher } from "@/layouts/org-switcher.tsx"
|
||||||
|
import { ThemePillSwitcher } from "@/layouts/theme-switcher"
|
||||||
import { Topbar } from "@/layouts/topbar.tsx"
|
import { Topbar } from "@/layouts/topbar.tsx"
|
||||||
import { NavLink, Outlet } from "react-router-dom"
|
import { NavLink, Outlet } from "react-router-dom"
|
||||||
|
|
||||||
@@ -147,6 +148,7 @@ export const AppShell = () => {
|
|||||||
<SidebarMenuButton asChild tooltip={n.label}>
|
<SidebarMenuButton asChild tooltip={n.label}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={n.to}
|
to={n.to}
|
||||||
|
target={n.target ? n.target : "_self"}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn("flex items-center gap-2", isActive && "text-primary")
|
cn("flex items-center gap-2", isActive && "text-primary")
|
||||||
}
|
}
|
||||||
@@ -160,6 +162,9 @@ export const AppShell = () => {
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
<div className="mt-auto flex items-center justify-center p-3">
|
||||||
|
<ThemePillSwitcher />
|
||||||
|
</div>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function asClipboardText(v?: VersionInfo) {
|
|||||||
return `v${v.version} (${shortCommit(v.commit)}) • built ${v.built} • ${v.go} ${v.goOS}/${v.goArch}`
|
return `v${v.version} (${shortCommit(v.commit)}) • built ${v.built} • ${v.go} ${v.goOS}/${v.goArch}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Footer = memo(function Footer({ className }: { className?: string }) {
|
export const Footer = memo(function Footer() {
|
||||||
const footerQ = useQuery({
|
const footerQ = useQuery({
|
||||||
queryKey: ["footer"],
|
queryKey: ["footer"],
|
||||||
queryFn: () => metaApi.footer() as Promise<VersionInfo>,
|
queryFn: () => metaApi.footer() as Promise<VersionInfo>,
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ import {
|
|||||||
import { AiOutlineCluster } from "react-icons/ai"
|
import { AiOutlineCluster } from "react-icons/ai"
|
||||||
import { GrUserWorker } from "react-icons/gr"
|
import { GrUserWorker } from "react-icons/gr"
|
||||||
import { MdOutlineDns } from "react-icons/md"
|
import { MdOutlineDns } from "react-icons/md"
|
||||||
|
import { SiSwagger } from "react-icons/si"
|
||||||
|
import { TbLoadBalancer } from "react-icons/tb"
|
||||||
|
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
to: string
|
to: string
|
||||||
label: string
|
label: string
|
||||||
icon: ComponentType<{ className?: string }>
|
icon: ComponentType<{ className?: string }>
|
||||||
|
target?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mainNav: NavItem[] = [
|
export const mainNav: NavItem[] = [
|
||||||
{ to: "/clusters", label: "Clusters", icon: AiOutlineCluster },
|
{ to: "/clusters", label: "Clusters", icon: AiOutlineCluster },
|
||||||
|
{ to: "/load-balancers", label: "Load Balancers", icon: TbLoadBalancer},
|
||||||
{ to: "/dns", label: "DNS", icon: MdOutlineDns },
|
{ to: "/dns", label: "DNS", icon: MdOutlineDns },
|
||||||
{ to: "/node-pools", label: "Node Pools", icon: BoxesIcon },
|
{ to: "/node-pools", label: "Node Pools", icon: BoxesIcon },
|
||||||
{ to: "/annotations", label: "Annotations", icon: ComponentIcon },
|
{ to: "/annotations", label: "Annotations", icon: ComponentIcon },
|
||||||
@@ -45,4 +49,5 @@ export const userNav: NavItem[] = [{ to: "/me", label: "Profile", icon: User2 }]
|
|||||||
export const adminNav: NavItem[] = [
|
export const adminNav: NavItem[] = [
|
||||||
{ to: "/admin/users", label: "Users Admin", icon: Users },
|
{ to: "/admin/users", label: "Users Admin", icon: Users },
|
||||||
{ to: "/admin/jobs", label: "Jobs Admin", icon: GrUserWorker },
|
{ to: "/admin/jobs", label: "Jobs Admin", icon: GrUserWorker },
|
||||||
|
{ to: "/docs", label: "API Docs ", icon: SiSwagger, target: "_blank" },
|
||||||
]
|
]
|
||||||
|
|||||||
78
ui/src/layouts/theme-switcher.tsx
Normal file
78
ui/src/layouts/theme-switcher.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { type ComponentType } from "react"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { Monitor, Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
type ThemeValue = "light" | "dark" | "system"
|
||||||
|
|
||||||
|
const options: { id: ThemeValue; icon: ComponentType<{ className?: string }>; label: string }[] = [
|
||||||
|
{ id: "light", icon: Sun, label: "Light" },
|
||||||
|
{ id: "dark", icon: Moon, label: "Dark" },
|
||||||
|
{ id: "system", icon: Monitor, label: "System" },
|
||||||
|
]
|
||||||
|
|
||||||
|
interface ThemePillSwitcherProps {
|
||||||
|
className?: string
|
||||||
|
variant?: "pill" | "wide"
|
||||||
|
ariaLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemePillSwitcher = ({
|
||||||
|
className = "",
|
||||||
|
variant = "pill",
|
||||||
|
ariaLabel = "Toggle theme",
|
||||||
|
}: ThemePillSwitcherProps) => {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
const currentTheme = (theme ?? "system") as ThemeValue
|
||||||
|
const isPill = variant === "pill"
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center",
|
||||||
|
isPill && "bg-muted/70 rounded-full p-1 text-xs shadow-sm",
|
||||||
|
!isPill && "gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
role="radiogroup"
|
||||||
|
>
|
||||||
|
{options.map(({ id, icon: Icon, label }) => {
|
||||||
|
const isActive = currentTheme === id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={isActive}
|
||||||
|
onClick={() => setTheme(id)}
|
||||||
|
aria-label={isPill ? label : undefined}
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:ring-ring focus-visible:ring-offset-background relative flex items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
|
isActive ? "text-foreground border" : "text-muted-foreground hover:text-foreground",
|
||||||
|
|
||||||
|
// --- Conditional Classes ---
|
||||||
|
// "pill" variant is a fixed 8x8 square
|
||||||
|
isPill && "h-8 w-8",
|
||||||
|
// "wide" variant has padding, a gap, and auto width
|
||||||
|
!isPill && "h-8 gap-2 px-3 text-sm font-medium"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive && (
|
||||||
|
<motion.span
|
||||||
|
layoutId="theme-switcher-pill"
|
||||||
|
className="bg-background absolute inset-0 rounded-full shadow-sm"
|
||||||
|
transition={{ type: "spring", stiffness: 350, damping: 26 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Icon className="relative z-10 h-4 w-4" />
|
||||||
|
{!isPill && <span className="relative z-10">{label}</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
import { ThemePillSwitcher } from "@/layouts/theme-switcher"
|
||||||
import { Link, useLocation } from "react-router-dom"
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
import { useMe } from "@/hooks/use-me.ts"
|
import { useMe } from "@/hooks/use-me.ts"
|
||||||
@@ -69,6 +70,7 @@ export const Topbar = () => {
|
|||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ThemePillSwitcher variant="wide" />
|
||||||
<Button variant="ghost" size="sm" asChild>
|
<Button variant="ghost" size="sm" asChild>
|
||||||
<Link to="/me">{isLoading ? "…" : me?.display_name || "Profile"}</Link>
|
<Link to="/me">{isLoading ? "…" : me?.display_name || "Profile"}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { annotationsApi } from "@/api/annotations.ts"
|
import { annotationsApi } from "@/api/annotations.ts"
|
||||||
import { labelsApi } from "@/api/labels.ts"
|
|
||||||
import type { DtoLabelResponse } from "@/sdk"
|
import type { DtoLabelResponse } from "@/sdk"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
1273
ui/src/pages/cluster-page.tsx
Normal file
1273
ui/src/pages/cluster-page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -192,7 +192,9 @@ function extractErr(e: any): string {
|
|||||||
try {
|
try {
|
||||||
const msg = (e as any)?.response?.data?.message || (e as any)?.message
|
const msg = (e as any)?.response?.data?.message || (e as any)?.message
|
||||||
if (msg) return String(msg)
|
if (msg) return String(msg)
|
||||||
} catch {}
|
} catch {
|
||||||
|
return "Unknown error"
|
||||||
|
}
|
||||||
return "Unknown error"
|
return "Unknown error"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +224,12 @@ export const DnsPage = () => {
|
|||||||
const r53Credentials = useMemo(() => (credentialQ.data ?? []).filter(isR53), [credentialQ.data])
|
const r53Credentials = useMemo(() => (credentialQ.data ?? []).filter(isR53), [credentialQ.data])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const setSelectedDns = () => {
|
||||||
if (!selected && domainsQ.data && domainsQ.data.length) {
|
if (!selected && domainsQ.data && domainsQ.data.length) {
|
||||||
setSelected(domainsQ.data[0]!)
|
setSelected(domainsQ.data[0]!)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
setSelectedDns()
|
||||||
}, [domainsQ.data, selected])
|
}, [domainsQ.data, selected])
|
||||||
|
|
||||||
const filteredDomains = useMemo(() => {
|
const filteredDomains = useMemo(() => {
|
||||||
171
ui/src/pages/docs-page.tsx
Normal file
171
ui/src/pages/docs-page.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { useEffect, useRef, useState, type FC } from "react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
|
import "rapidoc"
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select.tsx"
|
||||||
|
|
||||||
|
type RdThemeMode = "auto" | "light" | "dark"
|
||||||
|
|
||||||
|
export const DocsPage: FC = () => {
|
||||||
|
const rdRef = useRef<any>(null)
|
||||||
|
const { theme, systemTheme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
const [orgId, setOrgId] = useState("")
|
||||||
|
const [rdThemeMode, setRdThemeMode] = useState<RdThemeMode>("auto")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stateSetter = () => {
|
||||||
|
const stored = localStorage.getItem("autoglue.org")
|
||||||
|
if (stored) setOrgId(stored)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateSetter()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const rd = rdRef.current
|
||||||
|
if (!rd) return
|
||||||
|
|
||||||
|
let effectiveTheme: "light" | "dark" = "light"
|
||||||
|
if (rdThemeMode === "light") {
|
||||||
|
effectiveTheme = "light"
|
||||||
|
} else if (rdThemeMode === "dark") {
|
||||||
|
effectiveTheme = "dark"
|
||||||
|
} else {
|
||||||
|
const appTheme = theme === "system" ? systemTheme : theme
|
||||||
|
effectiveTheme = appTheme === "dark" ? "dark" : "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
rd.setAttribute("theme", effectiveTheme)
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const defaultServer = `${window.location.origin}/api/v1`
|
||||||
|
rd.setAttribute("default-api-server", defaultServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgId) {
|
||||||
|
rd.setAttribute("api-key-name", "X-ORG-ID")
|
||||||
|
rd.setAttribute("api-key-location", "header")
|
||||||
|
rd.setAttribute("api-key-value", orgId)
|
||||||
|
} else {
|
||||||
|
rd.removeAttribute("api-key-value")
|
||||||
|
}
|
||||||
|
}, [theme, systemTheme, rdThemeMode, orgId])
|
||||||
|
|
||||||
|
const handleSaveOrg = () => {
|
||||||
|
const trimmed = orgId.trim()
|
||||||
|
localStorage.setItem("autoglue.org", trimmed)
|
||||||
|
const rd = rdRef.current
|
||||||
|
if (!rd) return
|
||||||
|
|
||||||
|
if (trimmed) {
|
||||||
|
rd.setAttribute("api-key-value", trimmed)
|
||||||
|
} else {
|
||||||
|
rd.removeAttribute("api-key-value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetOrg = () => {
|
||||||
|
localStorage.removeItem("autoglue.org")
|
||||||
|
setOrgId("")
|
||||||
|
const rd = rdRef.current
|
||||||
|
if (!rd) return
|
||||||
|
rd.removeAttribute("api-key-value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-[100svh] flex-col">
|
||||||
|
{/* Control bar */}
|
||||||
|
<Card className="rounded-none border-b">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="flex flex-wrap items-center justify-between gap-4 text-base">
|
||||||
|
<span>AutoGlue API Docs</span>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-xs">
|
||||||
|
{/* Theme selector */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">Docs theme</span>
|
||||||
|
<Select
|
||||||
|
value={rdThemeMode}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const mode = v as RdThemeMode
|
||||||
|
setRdThemeMode(mode)
|
||||||
|
|
||||||
|
if (mode === "auto") {
|
||||||
|
setTheme("system")
|
||||||
|
} else {
|
||||||
|
setTheme(v)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[120px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="auto">Auto (match app)</SelectItem>
|
||||||
|
<SelectItem value="light">Light</SelectItem>
|
||||||
|
<SelectItem value="dark">Dark</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Org ID controls */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">Org ID (X-ORG-ID)</span>
|
||||||
|
<Input
|
||||||
|
className="h-8 w-80"
|
||||||
|
value={orgId}
|
||||||
|
onChange={(e) => setOrgId(e.target.value)}
|
||||||
|
placeholder="org_..."
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={handleSaveOrg}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={handleResetOrg}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-muted-foreground py-0 pb-2 text-xs">
|
||||||
|
Requests from <code><rapi-doc></code> will include:
|
||||||
|
<code className="ml-1">Cookie: ag_jwt=…</code> and{" "}
|
||||||
|
<code className="ml-1">X-ORG-ID={orgId}</code>
|
||||||
|
{!orgId && <> (set an Org ID above to send an X-ORG-ID header)</>}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* @ts-expect-error ts-2339 */}
|
||||||
|
<rapi-doc
|
||||||
|
ref={rdRef}
|
||||||
|
id="autoglue-docs"
|
||||||
|
spec-url="/swagger/openapi.json"
|
||||||
|
render-style="read"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
479
ui/src/pages/load-balancers-page.tsx
Normal file
479
ui/src/pages/load-balancers-page.tsx
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { loadBalancersApi } from "@/api/loadbalancers"
|
||||||
|
import type { DtoLoadBalancerResponse } from "@/sdk"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { CircleSlash2, Network, Pencil, Plus, Search } from "lucide-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { truncateMiddle } from "@/lib/utils"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
|
||||||
|
// --- schemas ---
|
||||||
|
|
||||||
|
const createLoadBalancerSchema = z.object({
|
||||||
|
name: z.string().trim().min(1, "Name is required").max(120, "Max 120 chars"),
|
||||||
|
kind: z.enum(["glueops", "public"]).default("public"),
|
||||||
|
public_ip_address: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Public IP/hostname is required")
|
||||||
|
.max(255, "Max 255 chars"),
|
||||||
|
private_ip_address: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Private IP/hostname is required")
|
||||||
|
.max(255, "Max 255 chars"),
|
||||||
|
})
|
||||||
|
type CreateLoadBalancerInput = z.input<typeof createLoadBalancerSchema>
|
||||||
|
|
||||||
|
const updateLoadBalancerSchema = createLoadBalancerSchema.partial()
|
||||||
|
type UpdateLoadBalancerValues = z.infer<typeof updateLoadBalancerSchema>
|
||||||
|
|
||||||
|
// --- badge ---
|
||||||
|
|
||||||
|
function LoadBalancerBadge({
|
||||||
|
lb,
|
||||||
|
}: {
|
||||||
|
lb: Pick<DtoLoadBalancerResponse, "name" | "kind" | "public_ip_address" | "private_ip_address">
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className="font-mono text-xs">
|
||||||
|
<Network className="mr-1 h-3 w-3" />
|
||||||
|
{lb.name} · {lb.kind} · {lb.public_ip_address} → {lb.private_ip_address}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadBalancersPage = () => {
|
||||||
|
const [filter, setFilter] = useState<string>("")
|
||||||
|
const [createOpen, setCreateOpen] = useState<boolean>(false)
|
||||||
|
const [updateOpen, setUpdateOpen] = useState<boolean>(false)
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
const lbsQ = useQuery({
|
||||||
|
queryKey: ["loadBalancers"],
|
||||||
|
queryFn: () => loadBalancersApi.listLoadBalancers(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Create ---
|
||||||
|
|
||||||
|
const createForm = useForm<CreateLoadBalancerInput>({
|
||||||
|
resolver: zodResolver(createLoadBalancerSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
kind: "public",
|
||||||
|
public_ip_address: "",
|
||||||
|
private_ip_address: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMut = useMutation({
|
||||||
|
mutationFn: (values: CreateLoadBalancerInput) => loadBalancersApi.createLoadBalancer(values),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await qc.invalidateQueries({ queryKey: ["loadBalancers"] })
|
||||||
|
createForm.reset()
|
||||||
|
setCreateOpen(false)
|
||||||
|
toast.success("Load balancer created successfully.")
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
toast.error(err?.message ?? "There was an error while creating the load balancer")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onCreateSubmit = (values: CreateLoadBalancerInput) => {
|
||||||
|
createMut.mutate(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update ---
|
||||||
|
|
||||||
|
const updateForm = useForm<UpdateLoadBalancerValues>({
|
||||||
|
resolver: zodResolver(updateLoadBalancerSchema),
|
||||||
|
defaultValues: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateMut = useMutation({
|
||||||
|
mutationFn: ({ id, values }: { id: string; values: UpdateLoadBalancerValues }) =>
|
||||||
|
loadBalancersApi.updateLoadBalancer(id, values),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await qc.invalidateQueries({ queryKey: ["loadBalancers"] })
|
||||||
|
updateForm.reset()
|
||||||
|
setUpdateOpen(false)
|
||||||
|
toast.success("Load balancer updated successfully.")
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
toast.error(err?.message ?? "There was an error while updating the load balancer")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const openEdit = (lb: DtoLoadBalancerResponse) => {
|
||||||
|
setEditingId(lb.id!)
|
||||||
|
updateForm.reset({
|
||||||
|
name: lb.name ?? "",
|
||||||
|
kind: (lb.kind as "public" | "glueops") ?? "public",
|
||||||
|
public_ip_address: lb.public_ip_address ?? "",
|
||||||
|
private_ip_address: lb.private_ip_address ?? "",
|
||||||
|
})
|
||||||
|
setUpdateOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete ---
|
||||||
|
|
||||||
|
const deleteMut = useMutation({
|
||||||
|
mutationFn: (id: string) => loadBalancersApi.deleteLoadBalancer(id),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await qc.invalidateQueries({ queryKey: ["loadBalancers"] })
|
||||||
|
setDeleteId(null)
|
||||||
|
toast.success("Load balancer deleted successfully.")
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
toast.error(err?.message ?? "There was an error while deleting the load balancer")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Filter ---
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const data = lbsQ.data ?? []
|
||||||
|
const q = filter.trim().toLowerCase()
|
||||||
|
|
||||||
|
return q
|
||||||
|
? data.filter((lb: any) => {
|
||||||
|
return (
|
||||||
|
lb.name?.toLowerCase().includes(q) ||
|
||||||
|
lb.kind?.toLowerCase().includes(q) ||
|
||||||
|
lb.public_ip_address?.toLowerCase().includes(q) ||
|
||||||
|
lb.private_ip_address?.toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: data
|
||||||
|
}, [filter, lbsQ.data])
|
||||||
|
|
||||||
|
if (lbsQ.isLoading) return <div className="p-6">Loading load balancers…</div>
|
||||||
|
if (lbsQ.error) return <div className="p-6 text-red-500">Error loading load balancers.</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 p-6">
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<h1 className="mb-4 text-2xl font-bold">Load Balancers</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute top-2.5 left-2 h-4 w-4 opacity-60" />
|
||||||
|
<Input
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
placeholder="Search load balancers"
|
||||||
|
className="w-64 pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button onClick={() => setCreateOpen(true)}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Load Balancer
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Load Balancer</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...createForm}>
|
||||||
|
<form className="space-y-4" onSubmit={createForm.handleSubmit(onCreateSubmit)}>
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="apps-lb-01" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="kind"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Kind</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value ?? "public"}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select kind" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="public">Public</SelectItem>
|
||||||
|
<SelectItem value="glueops">GlueOps</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="public_ip_address"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Public IP</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="1.2.3.4" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="private_ip_address"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Private IP</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="10.0.30.10" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={createMut.isPending}>
|
||||||
|
{createMut.isPending ? "Creating…" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-background overflow-hidden rounded-2xl border shadow-sm">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Kind</TableHead>
|
||||||
|
<TableHead>Public IP / Hostname</TableHead>
|
||||||
|
<TableHead>Private IP / Hostname</TableHead>
|
||||||
|
<TableHead>Summary</TableHead>
|
||||||
|
<TableHead className="w-[220px] text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filtered.map((lb: DtoLoadBalancerResponse) => (
|
||||||
|
<TableRow key={lb.id}>
|
||||||
|
<TableCell>{lb.name}</TableCell>
|
||||||
|
<TableCell>{lb.kind}</TableCell>
|
||||||
|
<TableCell>{lb.public_ip_address}</TableCell>
|
||||||
|
<TableCell>{lb.private_ip_address}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LoadBalancerBadge lb={lb} />
|
||||||
|
{lb.id && (
|
||||||
|
<code className="text-muted-foreground text-xs">
|
||||||
|
{truncateMiddle(lb.id, 6)}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => openEdit(lb)}>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" /> Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDeleteId(lb.id!)}
|
||||||
|
disabled={deleteMut.isPending && deleteId === lb.id}
|
||||||
|
>
|
||||||
|
{deleteMut.isPending && deleteId === lb.id ? "Deleting…" : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-muted-foreground py-10 text-center">
|
||||||
|
<CircleSlash2 className="mx-auto mb-2 h-6 w-6 opacity-60" />
|
||||||
|
No load balancers match your search.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Update dialog */}
|
||||||
|
<Dialog open={updateOpen} onOpenChange={setUpdateOpen}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Load Balancer</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...updateForm}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={updateForm.handleSubmit((values) => {
|
||||||
|
if (!editingId) return
|
||||||
|
updateMut.mutate({ id: editingId, values })
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={updateForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="apps-lb-01" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={updateForm.control}
|
||||||
|
name="kind"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Kind</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value ?? "public"}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select kind" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="public">Public</SelectItem>
|
||||||
|
<SelectItem value="glueops">GlueOps</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={updateForm.control}
|
||||||
|
name="public_ip_address"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Public IP / Hostname</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="1.2.3.4 or apps.example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={updateForm.control}
|
||||||
|
name="private_ip_address"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Private IP / Hostname</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="10.0.30.10" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setUpdateOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={updateMut.isPending}>
|
||||||
|
{updateMut.isPending ? "Saving…" : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete confirm dialog */}
|
||||||
|
<Dialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete load balancer</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
This action cannot be undone. Are you sure you want to delete this load balancer?
|
||||||
|
</p>
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setDeleteId(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => deleteId && deleteMut.mutate(deleteId)}
|
||||||
|
disabled={deleteMut.isPending}
|
||||||
|
>
|
||||||
|
{deleteMut.isPending ? "Deleting…" : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useEffect, useMemo } from "react"
|
import { useEffect } from "react"
|
||||||
import { credentialsApi } from "@/api/credentials.ts"
|
|
||||||
import { withRefresh } from "@/api/with-refresh.ts"
|
import { withRefresh } from "@/api/with-refresh.ts"
|
||||||
import { orgStore } from "@/auth/org.ts"
|
import { orgStore } from "@/auth/org.ts"
|
||||||
import type { DtoCredentialOut } from "@/sdk"
|
|
||||||
import { makeOrgsApi } from "@/sdkClient.ts"
|
import { makeOrgsApi } from "@/sdkClient.ts"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
@@ -22,6 +20,7 @@ import {
|
|||||||
} from "@/components/ui/form.tsx"
|
} from "@/components/ui/form.tsx"
|
||||||
import { Input } from "@/components/ui/input.tsx"
|
import { Input } from "@/components/ui/input.tsx"
|
||||||
|
|
||||||
|
/*
|
||||||
const isS3 = (c: DtoCredentialOut) =>
|
const isS3 = (c: DtoCredentialOut) =>
|
||||||
c.provider === "aws" &&
|
c.provider === "aws" &&
|
||||||
c.scope_kind === "service" &&
|
c.scope_kind === "service" &&
|
||||||
@@ -35,6 +34,7 @@ const isS3 = (c: DtoCredentialOut) =>
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
*/
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().min(1, "Required"),
|
name: z.string().min(1, "Required"),
|
||||||
@@ -54,13 +54,14 @@ export const OrgSettings = () => {
|
|||||||
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
|
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
const credentialQ = useQuery({
|
const credentialQ = useQuery({
|
||||||
queryKey: ["credentials", "s3"],
|
queryKey: ["credentials", "s3"],
|
||||||
queryFn: () => credentialsApi.listCredentials(), // client-side filter
|
queryFn: () => credentialsApi.listCredentials(), // client-side filter
|
||||||
})
|
})
|
||||||
|
|
||||||
const s3Credentials = useMemo(() => (credentialQ.data ?? []).filter(isS3), [credentialQ.data])
|
const s3Credentials = useMemo(() => (credentialQ.data ?? []).filter(isS3), [credentialQ.data])
|
||||||
|
*/
|
||||||
const form = useForm<Values>({
|
const form = useForm<Values>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -76,7 +77,7 @@ export const OrgSettings = () => {
|
|||||||
domain: q.data.domain ?? "",
|
domain: q.data.domain ?? "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [q.data])
|
}, [q.data, form])
|
||||||
|
|
||||||
const updateMut = useMutation({
|
const updateMut = useMutation({
|
||||||
mutationFn: (v: Partial<Values>) => api.updateOrg({ id: orgId!, body: v }),
|
mutationFn: (v: Partial<Values>) => api.updateOrg({ id: orgId!, body: v }),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
import { Plus, Search } from "lucide-react"
|
import { Plus, Search } from "lucide-react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
@@ -136,8 +136,17 @@ export const ServerPage = () => {
|
|||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleIsBastion = createForm.watch("role") === "bastion"
|
const watchedRoleCreate = useWatch({
|
||||||
const pubCreate = createForm.watch("public_ip_address")?.trim() ?? ""
|
control: createForm.control,
|
||||||
|
name: "role",
|
||||||
|
})
|
||||||
|
const roleIsBastion = watchedRoleCreate === "bastion"
|
||||||
|
|
||||||
|
const watchedPublicIpCreate = useWatch({
|
||||||
|
control: createForm.control,
|
||||||
|
name: "public_ip_address",
|
||||||
|
})
|
||||||
|
const pubCreate = watchedPublicIpCreate?.trim() ?? ""
|
||||||
const needPubCreate = roleIsBastion && pubCreate === ""
|
const needPubCreate = roleIsBastion && pubCreate === ""
|
||||||
|
|
||||||
const createMut = useMutation({
|
const createMut = useMutation({
|
||||||
@@ -160,8 +169,19 @@ export const ServerPage = () => {
|
|||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleIsBastionU = updateForm.watch("role") === "bastion"
|
const watchedRoleUpdate = useWatch({
|
||||||
const pubUpdate = updateForm.watch("public_ip_address")?.trim() ?? ""
|
control: updateForm.control,
|
||||||
|
name: "role",
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchedPublicIpAddressUpdate = useWatch({
|
||||||
|
control: updateForm.control,
|
||||||
|
name: "public_ip_address",
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleIsBastionU = watchedRoleUpdate === "bastion"
|
||||||
|
|
||||||
|
const pubUpdate = watchedPublicIpAddressUpdate?.trim() ?? ""
|
||||||
const needPubUpdate = roleIsBastionU && pubUpdate === ""
|
const needPubUpdate = roleIsBastionU && pubUpdate === ""
|
||||||
|
|
||||||
const updateMut = useMutation({
|
const updateMut = useMutation({
|
||||||
@@ -4,11 +4,10 @@ import type { DtoCreateSSHRequest, DtoSshRevealResponse } from "@/sdk"
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Download, Eye, Loader2, Plus, Trash2 } from "lucide-react"
|
import { Download, Eye, Loader2, Plus, Trash2 } from "lucide-react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { truncateMiddle } from "@/lib/utils.ts"
|
|
||||||
import { Badge } from "@/components/ui/badge.tsx"
|
import { Badge } from "@/components/ui/badge.tsx"
|
||||||
import { Button } from "@/components/ui/button.tsx"
|
import { Button } from "@/components/ui/button.tsx"
|
||||||
import {
|
import {
|
||||||
@@ -105,6 +104,11 @@ export const SshPage = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const watchedType = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "type",
|
||||||
|
})
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: async (values: CreateKeyInput) => {
|
mutationFn: async (values: CreateKeyInput) => {
|
||||||
const payload: DtoCreateSSHRequest = {
|
const payload: DtoCreateSSHRequest = {
|
||||||
@@ -257,7 +261,7 @@ export const SshPage = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={field.value}
|
value={field.value}
|
||||||
disabled={form.watch("type") === "ed25519"}
|
disabled={watchedType === "ed25519"}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
@@ -316,7 +320,6 @@ export const SshPage = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{filtered.map((k) => {
|
{filtered.map((k) => {
|
||||||
const keyType = getKeyType(k.public_key!)
|
const keyType = getKeyType(k.public_key!)
|
||||||
const truncated = truncateMiddle(k.public_key!, 18)
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={k.id}>
|
<TableRow key={k.id}>
|
||||||
<TableCell className="font-medium">{k.name || "—"}</TableCell>
|
<TableCell className="font-medium">{k.name || "—"}</TableCell>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from "react"
|
import { type ReactNode } from "react"
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
|
||||||
export type Theme = "light" | "dark" | "system"
|
export type Theme = "light" | "dark" | "system"
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {
|
|||||||
AnnotationsApi,
|
AnnotationsApi,
|
||||||
ArcherAdminApi,
|
ArcherAdminApi,
|
||||||
AuthApi,
|
AuthApi,
|
||||||
|
ClustersApi,
|
||||||
Configuration,
|
Configuration,
|
||||||
CredentialsApi,
|
CredentialsApi,
|
||||||
DNSApi,
|
DNSApi,
|
||||||
LabelsApi,
|
LabelsApi,
|
||||||
|
LoadBalancersApi,
|
||||||
MeApi,
|
MeApi,
|
||||||
MeAPIKeysApi,
|
MeAPIKeysApi,
|
||||||
MetaApi,
|
MetaApi,
|
||||||
@@ -123,3 +125,11 @@ export function makeCredentialsApi() {
|
|||||||
export function makeDnsApi() {
|
export function makeDnsApi() {
|
||||||
return makeApiClient(DNSApi)
|
return makeApiClient(DNSApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeLoadBalancerApi() {
|
||||||
|
return makeApiClient(LoadBalancersApi)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeClusterApi() {
|
||||||
|
return makeApiClient(ClustersApi)
|
||||||
|
}
|
||||||
30
ui/src/types/rapidoc.d.ts
vendored
Normal file
30
ui/src/types/rapidoc.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
"rapi-doc": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
|
||||||
|
"spec-url"?: string
|
||||||
|
"render-style"?: string
|
||||||
|
theme?: string
|
||||||
|
"show-header"?: string | boolean
|
||||||
|
"persist-auth"?: string | boolean
|
||||||
|
"allow-advanced-search"?: string | boolean
|
||||||
|
"schema-description-expanded"?: string | boolean
|
||||||
|
"allow-schema-description-expand-toggle"?: string | boolean
|
||||||
|
"allow-spec-file-download"?: string | boolean
|
||||||
|
"allow-spec-file-load"?: string | boolean
|
||||||
|
"allow-spec-url-load"?: string | boolean
|
||||||
|
"allow-try"?: string | boolean
|
||||||
|
"schema-style"?: string
|
||||||
|
"fetch-credentials"?: string
|
||||||
|
"default-api-server"?: string
|
||||||
|
"api-key-name"?: string
|
||||||
|
"api-key-location"?: string
|
||||||
|
"api-key-value"?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -28,5 +28,5 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "src/types"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
}
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src", "src/types"]
|
||||||
}
|
}
|
||||||
|
|||||||
935
ui/yarn.lock
935
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user