mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
feat: Complete AG Loadbalancer & Cluster API
Refactor routing logic (Chi can be a pain when you're managing large sets of routes, but its one of the better options when considering a potential gRPC future)
Upgrade API Generation to fully support OAS3.1
Update swagger interface to RapiDoc - the old swagger interface doesnt support OAS3.1 yet
Docs are now embedded as part of the UI - once logged in they pick up the cookies and org id from what gets set by the UI, but you can override it
Other updates include better portability of the db-studio
Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
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
|
||||||
6
Makefile
6
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
|
||||||
@@ -112,10 +113,11 @@ $(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 @v2.0.0-rc4..."; \
|
||||||
$(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/swagger.* docs/docs.go
|
||||||
@swag init -g $(MAIN) -o docs
|
@swag fmt -d .
|
||||||
|
@swag init $(SWAG_FLAGS) -g $(MAIN) -o docs
|
||||||
|
|
||||||
# --- 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,
|
||||||
|
|||||||
16
docs/docs.go
16
docs/docs.go
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4255
docs/swagger.yaml
4255
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
62
go.mod
62
go.mod
@@ -3,6 +3,7 @@ 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.18
|
||||||
@@ -11,6 +12,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0
|
github.com/coreos/go-oidc/v3 v3.16.0
|
||||||
github.com/dyaksa/archer v1.1.3
|
github.com/dyaksa/archer v1.1.3
|
||||||
|
github.com/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
|
||||||
@@ -55,13 +68,19 @@ require (
|
|||||||
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
39
internal/api/mount_cluster_routes.go
Normal file
39
internal/api/mount_cluster_routes.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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))
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||||
r.Get("/swagger/swagger.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)
|
|
||||||
_, _ = w.Write([]byte(`<!doctype html><html><body><script>
|
|
||||||
(function(){
|
(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,30 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|||||||
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" enums:"glueops|public"`
|
||||||
|
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"
|
||||||
|
|||||||
283
internal/handlers/load_balancers.go
Normal file
283
internal/handlers/load_balancers.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"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" {
|
||||||
|
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 {
|
||||||
|
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,28 +6,47 @@ 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"`
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Status string `json:"status"`
|
|
||||||
CaptainDomain string `gorm:"not null" json:"captain_domain"` // nonprod.earth.onglueops.rocks
|
|
||||||
AppsLoadBalancer string `json:"cluster_load_balancer"` // {public_ip: 1.2.3.4, private_ip: 10.0.30.1, name: apps.CaqptainDomain}
|
|
||||||
GlueOpsLoadBalancer string `json:"control_load_balancer"` // {public_ip: 5.6.7.8, private_ip: 10.0.22.1, name: CaptainDomain}
|
|
||||||
|
|
||||||
ControlPlane string `json:"control_plane"` // <- dns cntlpn
|
Status string `gorm:"type:varchar(20);not null;default:'pre_pending'" json:"status"`
|
||||||
|
LastError string `gorm:"type:text;not null;default:''" json:"last_error"`
|
||||||
|
|
||||||
|
CaptainDomainID *uuid.UUID `gorm:"type:uuid" json:"captain_domain_id"`
|
||||||
|
CaptainDomain Domain `gorm:"foreignKey:CaptainDomainID" json:"captain_domain"`
|
||||||
|
ControlPlaneRecordSetID *uuid.UUID `gorm:"type:uuid" json:"control_plane_record_set_id,omitempty"`
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
15
main.go
15
main.go
@@ -1,21 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/glueops/autoglue/cmd"
|
"github.com/glueops/autoglue/cmd"
|
||||||
"github.com/glueops/autoglue/docs"
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title AutoGlue API
|
// @title AutoGlue API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @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 +37,5 @@ import (
|
|||||||
// @description Org-level secret
|
// @description Org-level secret
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = godotenv.Load()
|
|
||||||
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" ]
|
||||||
|
|||||||
430
schema.sql
Normal file
430
schema.sql
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
-- 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 "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 "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 "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 "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 "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" text NULL,
|
||||||
|
"captain_domain" text NOT NULL,
|
||||||
|
"apps_load_balancer" text NULL,
|
||||||
|
"glue_ops_load_balancer" text NULL,
|
||||||
|
"control_plane" text NULL,
|
||||||
|
"random_token" text NULL,
|
||||||
|
"certificate_key" text NULL,
|
||||||
|
"encrypted_kubeconfig" text NULL,
|
||||||
|
"kube_iv" text NULL,
|
||||||
|
"kube_tag" text NULL,
|
||||||
|
"bastion_server_id" uuid NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
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_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 "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 "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 "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 "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 "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 "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.9",
|
||||||
"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",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
|||||||
import { Login } from "@/pages/auth/login.tsx"
|
import { Login } from "@/pages/auth/login.tsx"
|
||||||
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
||||||
import { DnsPage } from "@/pages/dns/dns-page.tsx"
|
import { DnsPage } from "@/pages/dns/dns-page.tsx"
|
||||||
|
import { DocsPage } from "@/pages/docs/docs-page.tsx"
|
||||||
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
||||||
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
||||||
import { MePage } from "@/pages/me/me-page.tsx"
|
import { MePage } from "@/pages/me/me-page.tsx"
|
||||||
@@ -21,6 +22,8 @@ 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 />} />
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,11 +15,13 @@ 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"
|
||||||
|
|
||||||
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[] = [
|
||||||
@@ -45,4 +47,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"
|
||||||
|
|||||||
@@ -1,66 +1,28 @@
|
|||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react";
|
||||||
import { credentialsApi } from "@/api/credentials"
|
import { credentialsApi } from "@/api/credentials";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import { AlertTriangle, Eye, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2 } from "lucide-react";
|
||||||
AlertTriangle,
|
import { Controller, useForm } from "react-hook-form";
|
||||||
Eye,
|
import { toast } from "sonner";
|
||||||
Loader2,
|
import { z } from "zod";
|
||||||
MoreHorizontal,
|
|
||||||
Pencil,
|
|
||||||
Plus,
|
|
||||||
Search,
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
Trash2,
|
import { Badge } from "@/components/ui/badge";
|
||||||
} from "lucide-react"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Controller, useForm } from "react-hook-form"
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { toast } from "sonner"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { z } from "zod"
|
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 { Switch } from "@/components/ui/switch";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu"
|
|
||||||
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 { Switch } from "@/components/ui/switch"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
|
|
||||||
// -------------------- Constants --------------------
|
// -------------------- Constants --------------------
|
||||||
|
|
||||||
@@ -192,7 +154,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/docs-page.tsx
Normal file
171
ui/src/pages/docs/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/swagger.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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
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