feat: add credentials management

Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
allanice001
2025-11-09 21:46:31 +00:00
parent 56ea963b47
commit bc72df3c9a
43 changed files with 4671 additions and 15 deletions

View File

@@ -70,7 +70,7 @@ export GO_POST_PROCESS_FILE := gofmt -w
.DEFAULT_GOAL := help
# --- version metadata (ldflags) ---
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "none")
DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
BUILT_BY := $(shell whoami)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -49,6 +49,39 @@ definitions:
example: https://accounts.google.com/o/oauth2/v2/auth?client_id=...
type: string
type: object
dto.ClusterResponse:
properties:
bastion_server:
$ref: '#/definitions/dto.ServerResponse'
captain_domain:
type: string
certificate_key:
type: string
cluster_load_balancer:
type: string
control_load_balancer:
type: string
created_at:
type: string
id:
type: string
name:
type: string
node_pools:
items:
$ref: '#/definitions/dto.NodePoolResponse'
type: array
provider:
type: string
random_token:
type: string
region:
type: string
status:
type: string
updated_at:
type: string
type: object
dto.CreateAnnotationRequest:
properties:
key:
@@ -56,6 +89,75 @@ definitions:
value:
type: string
type: object
dto.CreateClusterRequest:
properties:
captain_domain:
type: string
cluster_load_balancer:
type: string
control_load_balancer:
type: string
name:
type: string
provider:
type: string
region:
type: string
status:
type: string
type: object
dto.CreateCredentialRequest:
properties:
account_id:
maxLength: 32
type: string
kind:
description: aws_access_key, api_token, basic_auth, oauth2
type: string
name:
description: human label
maxLength: 100
type: string
provider:
enum:
- aws
- cloudflare
- hetzner
- digitalocean
- generic
type: string
region:
maxLength: 32
type: string
schema_version:
description: secret schema version
minimum: 1
type: integer
scope:
description: '{"service":"route53"} or {"arn":"..."}'
type: object
scope_kind:
enum:
- provider
- service
- resource
type: string
scope_version:
description: scope schema version
minimum: 1
type: integer
secret:
description: encrypted later
type: object
required:
- kind
- provider
- schema_version
- scope
- scope_kind
- scope_version
- secret
type: object
dto.CreateLabelRequest:
properties:
key:
@@ -124,7 +226,46 @@ definitions:
value:
type: string
type: object
dto.CredentialOut:
properties:
account_id:
type: string
created_at:
type: string
id:
type: string
kind:
type: string
name:
type: string
provider:
type: string
region:
type: string
schema_version:
type: integer
scope:
type: object
scope_kind:
type: string
scope_version:
type: integer
updated_at:
type: string
type: object
dto.EnqueueRequest:
properties:
payload:
type: object
queue:
example: default
type: string
run_at:
example: "2025-11-05T08:00:00Z"
type: string
type:
example: email.send
type: string
type: object
dto.JWK:
properties:
@@ -416,6 +557,24 @@ definitions:
value:
type: string
type: object
dto.UpdateCredentialRequest:
properties:
account_id:
type: string
name:
type: string
region:
type: string
scope:
type: object
scope_kind:
type: string
scope_version:
type: integer
secret:
description: set if rotating
type: object
type: object
dto.UpdateLabelRequest:
properties:
key:
@@ -1320,6 +1479,339 @@ paths:
summary: Rotate refresh token
tags:
- Auth
/clusters:
get:
description: Returns clusters for the organization in X-Org-ID. Filter by `q`
(name contains).
operationId: ListClusters
parameters:
- description: Organization UUID
in: header
name: X-Org-ID
type: string
- description: Name contains (case-insensitive)
in: query
name: q
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.ClusterResponse'
type: array
"401":
description: Unauthorized
schema:
type: string
"403":
description: organization required
schema:
type: string
"500":
description: failed to list clusters
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: List clusters (org scoped)
tags:
- Clusters
post:
consumes:
- application/json
description: Creates a cluster. If `kubeconfig` is provided, it will be encrypted
per-organization and stored securely (never returned).
operationId: CreateCluster
parameters:
- description: Organization UUID
in: header
name: X-Org-ID
type: string
- description: payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.CreateClusterRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/dto.ClusterResponse'
"400":
description: invalid json
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"403":
description: organization required
schema:
type: string
"500":
description: create failed
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: Create cluster (org scoped)
tags:
- Clusters
/credentials:
get:
consumes:
- application/json
description: Returns credential metadata for the current org. Secrets are never
returned.
operationId: ListCredentials
parameters:
- description: Organization ID (UUID)
in: header
name: X-Org-ID
type: string
- description: Filter by provider (e.g., aws)
in: query
name: provider
type: string
- description: Filter by kind (e.g., aws_access_key)
in: query
name: kind
type: string
- description: Filter by scope kind (provider/service/resource)
in: query
name: scope_kind
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.CredentialOut'
type: array
"401":
description: Unauthorized
schema:
type: string
"403":
description: organization required
schema:
type: string
"500":
description: internal server error
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: List credentials (metadata only)
tags:
- Credentials
post:
consumes:
- application/json
operationId: CreateCredential
parameters:
- description: Organization ID (UUID)
in: header
name: X-Org-ID
type: string
- description: Credential payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.CreateCredentialRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/dto.CredentialOut'
"401":
description: Unauthorized
schema:
type: string
"403":
description: organization required
schema:
type: string
"500":
description: internal server error
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: Create a credential (encrypts secret)
tags:
- Credentials
/credentials/{id}:
delete:
consumes:
- application/json
operationId: DeleteCredential
parameters:
- description: Organization ID (UUID)
in: header
name: X-Org-ID
type: string
- description: Credential ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
"404":
description: not found
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: Delete credential
tags:
- Credentials
get:
consumes:
- application/json
operationId: GetCredential
parameters:
- description: Organization ID (UUID)
in: header
name: X-Org-ID
type: string
- description: Credential ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.CredentialOut'
"401":
description: Unauthorized
schema:
type: string
"403":
description: organization required
schema:
type: string
"500":
description: internal server error
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: Get credential by ID (metadata only)
tags:
- Credentials
patch:
consumes:
- application/json
operationId: UpdateCredential
parameters:
- description: Organization ID (UUID)
in: header
name: X-Org-ID
type: string
- description: Credential ID (UUID)
in: path
name: id
required: true
type: string
- description: Fields to update
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.UpdateCredentialRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.CredentialOut'
"403":
description: X-Org-ID required
schema:
type: string
"404":
description: not found
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: Update credential metadata and/or rotate secret
tags:
- Credentials
/credentials/{id}/reveal:
post:
consumes:
- application/json
operationId: RevealCredential
parameters:
- description: Organization ID (UUID)
in: header
name: X-Org-ID
type: string
- description: Credential ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"403":
description: organization required
schema:
type: string
"404":
description: not found
schema:
type: string
security:
- BearerAuth: []
- OrgKeyAuth: []
- OrgSecretAuth: []
summary: Reveal decrypted secret (one-time read)
tags:
- Credentials
/healthz:
get:
consumes:

7
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/httprate v0.15.0
github.com/go-playground/validator/v10 v10.28.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
@@ -18,7 +19,7 @@ require (
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag/v2 v2.0.0-rc4
golang.org/x/crypto v0.43.0
golang.org/x/oauth2 v0.32.0
golang.org/x/oauth2 v0.33.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
@@ -29,11 +30,14 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
@@ -46,6 +50,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect

16
go.sum
View File

@@ -22,6 +22,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
@@ -43,6 +45,14 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -89,6 +99,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -181,8 +193,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -127,6 +127,16 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
})
})
v1.Route("/credentials", func(c chi.Router) {
c.Use(authOrg)
c.Get("/", handlers.ListCredentials(db))
c.Post("/", handlers.CreateCredential(db))
c.Get("/{id}", handlers.GetCredential(db))
c.Patch("/{id}", handlers.UpdateCredential(db))
c.Delete("/{id}", handlers.DeleteCredential(db))
c.Post("/{id}/reveal", handlers.RevealCredential(db))
})
v1.Route("/ssh", func(s chi.Router) {
s.Use(authOrg)
s.Get("/", handlers.ListPublicSshKeys(db))

View File

@@ -40,6 +40,7 @@ func NewRuntime() *Runtime {
&models.Annotation{},
&models.NodePool{},
&models.Cluster{},
&models.Credential{},
)
if err != nil {

View File

@@ -0,0 +1,186 @@
package handlers
import (
"net/http"
"strings"
"time"
"github.com/glueops/autoglue/internal/api/httpmiddleware"
"github.com/glueops/autoglue/internal/common"
"github.com/glueops/autoglue/internal/handlers/dto"
"github.com/glueops/autoglue/internal/models"
"github.com/glueops/autoglue/internal/utils"
"gorm.io/gorm"
)
// ListClusters godoc
//
// @ID ListClusters
// @Summary List clusters (org scoped)
// @Description Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).
// @Tags Clusters
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param q query string false "Name contains (case-insensitive)"
// @Success 200 {array} dto.ClusterResponse
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "failed to list clusters"
// @Router /clusters [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListClusters(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
q := db.Where("organization_id = ?", orgID)
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
q = q.Where(`name ILIKE ?`, "%"+needle+"%")
}
var rows []models.Cluster
if err := q.
Preload("NodePools").
Preload("NodePools.Labels").
Preload("NodePools.Annotations").
Preload("NodePools.Labels").
Preload("NodePools.Taints").
Preload("NodePools.Servers").
Preload("BastionServer").
Find(&rows).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.ClusterResponse, 0, len(rows))
for _, row := range rows {
out = append(out, clusterToDTO(row))
}
}
}
// CreateCluster godoc
//
// @ID CreateCluster
// @Summary Create cluster (org scoped)
// @Description Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).
// @Tags Clusters
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param body body dto.CreateClusterRequest true "payload"
// @Success 201 {object} dto.ClusterResponse
// @Failure 400 {string} string "invalid json"
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "create failed"
// @Router /clusters [post]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func CreateCluster(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
}
// -- Helpers
func clusterToDTO(c models.Cluster) dto.ClusterResponse {
var bastion *dto.ServerResponse
if c.BastionServer != nil {
b := serverToDTO(*c.BastionServer)
bastion = &b
}
nps := make([]dto.NodePoolResponse, 0, len(c.NodePools))
for _, np := range c.NodePools {
nps = append(nps, nodePoolToDTO(np))
}
return dto.ClusterResponse{
ID: c.ID,
Name: c.Name,
Provider: c.Provider,
Region: c.Region,
Status: c.Status,
CaptainDomain: c.CaptainDomain,
ClusterLoadBalancer: c.ClusterLoadBalancer,
RandomToken: c.RandomToken,
CertificateKey: c.CertificateKey,
ControlLoadBalancer: c.ControlLoadBalancer,
NodePools: nps,
BastionServer: bastion,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func nodePoolToDTO(np models.NodePool) dto.NodePoolResponse {
labels := make([]dto.LabelResponse, 0, len(np.Labels))
for _, l := range np.Labels {
labels = append(labels, dto.LabelResponse{
Key: l.Key,
Value: l.Value,
})
}
annotations := make([]dto.AnnotationResponse, 0, len(np.Annotations))
for _, a := range np.Annotations {
annotations = append(annotations, dto.AnnotationResponse{
Key: a.Key,
Value: a.Value,
})
}
taints := make([]dto.TaintResponse, 0, len(np.Taints))
for _, t := range np.Taints {
taints = append(taints, dto.TaintResponse{
Key: t.Key,
Value: t.Value,
Effect: t.Effect,
})
}
servers := make([]dto.ServerResponse, 0, len(np.Servers))
for _, s := range np.Servers {
servers = append(servers, serverToDTO(s))
}
return dto.NodePoolResponse{
AuditFields: common.AuditFields{
ID: np.ID,
OrganizationID: np.OrganizationID,
CreatedAt: np.CreatedAt,
UpdatedAt: np.UpdatedAt,
},
Name: np.Name,
Role: dto.NodeRole(np.Role),
Labels: labels,
Annotations: annotations,
Taints: taints,
Servers: servers,
}
}
func serverToDTO(s models.Server) dto.ServerResponse {
return dto.ServerResponse{
ID: s.ID,
Hostname: s.Hostname,
PrivateIPAddress: s.PrivateIPAddress,
PublicIPAddress: s.PublicIPAddress,
Role: s.Role,
Status: s.Status,
SSHUser: s.SSHUser,
SshKeyID: s.SshKeyID,
CreatedAt: s.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: s.UpdatedAt.UTC().Format(time.RFC3339),
}
}

View File

@@ -0,0 +1,561 @@
package handlers
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"time"
"github.com/glueops/autoglue/internal/api/httpmiddleware"
"github.com/glueops/autoglue/internal/handlers/dto"
"github.com/glueops/autoglue/internal/models"
"github.com/glueops/autoglue/internal/utils"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// ListCredentials godoc
// @ID ListCredentials
// @Summary List credentials (metadata only)
// @Description Returns credential metadata for the current org. Secrets are never returned.
// @Tags Credentials
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization ID (UUID)"
// @Param provider query string false "Filter by provider (e.g., aws)"
// @Param kind query string false "Filter by kind (e.g., aws_access_key)"
// @Param scope_kind query string false "Filter by scope kind (provider/service/resource)"
// @Success 200 {array} dto.CredentialOut
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "internal server error"
// @Router /credentials [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListCredentials(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
q := db.Where("organization_id = ?", orgID)
if v := r.URL.Query().Get("provider"); v != "" {
q = q.Where("provider = ?", v)
}
if v := r.URL.Query().Get("kind"); v != "" {
q = q.Where("kind = ?", v)
}
if v := r.URL.Query().Get("scope_kind"); v != "" {
q = q.Where("scope_kind = ?", v)
}
var rows []models.Credential
if err := q.Order("updated_at DESC").Find(&rows).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
return
}
out := make([]dto.CredentialOut, 0, len(rows))
for i := range rows {
out = append(out, credOut(&rows[i]))
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// GetCredential godoc
// @ID GetCredential
// @Summary Get credential by ID (metadata only)
// @Tags Credentials
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization ID (UUID)"
// @Param id path string true "Credential ID (UUID)"
// @Success 200 {object} dto.CredentialOut
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "internal server error"
// @Router /credentials/{id} [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func GetCredential(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
}
idStr := chi.URLParam(r, "id")
id, err := uuid.Parse(idStr)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
return
}
var row models.Credential
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", "credential not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
return
}
utils.WriteJSON(w, http.StatusOK, credOut(&row))
}
}
// CreateCredential godoc
// @ID CreateCredential
// @Summary Create a credential (encrypts secret)
// @Tags Credentials
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization ID (UUID)"
// @Param body body dto.CreateCredentialRequest true "Credential payload"
// @Success 201 {object} dto.CredentialOut
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "internal server error"
// @Router /credentials [post]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func CreateCredential(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.CreateCredentialRequest
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
return
}
if err := dto.Validate.Struct(in); err != nil {
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
return
}
cred, err := SaveCredentialWithScope(
r.Context(), db, orgID,
in.Provider, in.Kind, in.SchemaVersion,
in.ScopeKind, in.ScopeVersion, json.RawMessage(in.Scope), json.RawMessage(in.Secret),
in.Name, in.AccountID, in.Region,
)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "save_failed", err.Error())
return
}
utils.WriteJSON(w, http.StatusCreated, credOut(cred))
}
}
// UpdateCredential godoc
// @ID UpdateCredential
// @Summary Update credential metadata and/or rotate secret
// @Tags Credentials
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization ID (UUID)"
// @Param id path string true "Credential ID (UUID)"
// @Param body body dto.UpdateCredentialRequest true "Fields to update"
// @Success 200 {object} dto.CredentialOut
// @Failure 403 {string} string "X-Org-ID required"
// @Failure 404 {string} string "not found"
// @Router /credentials/{id} [patch]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func UpdateCredential(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.Credential
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", "credential not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
return
}
var in dto.UpdateCredentialRequest
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
return
}
// Update metadata
if in.Name != nil {
row.Name = *in.Name
}
if in.AccountID != nil {
row.AccountID = *in.AccountID
}
if in.Region != nil {
row.Region = *in.Region
}
// Update scope (re-validate + fingerprint)
if in.ScopeKind != nil || in.Scope != nil || in.ScopeVersion != nil {
newKind := row.ScopeKind
if in.ScopeKind != nil {
newKind = *in.ScopeKind
}
newVersion := row.ScopeVersion
if in.ScopeVersion != nil {
newVersion = *in.ScopeVersion
}
if in.Scope == nil {
utils.WriteError(w, http.StatusBadRequest, "validation_error", "scope must be provided when changing scope kind/version")
return
}
prScopes := dto.ScopeRegistry[row.Provider]
kScopes := prScopes[newKind]
sdef := kScopes[newVersion]
dst := sdef.New()
if err := json.Unmarshal(*in.Scope, dst); err != nil {
utils.WriteError(w, http.StatusBadRequest, "invalid_scope_json", err.Error())
return
}
if err := sdef.Validate(dst); err != nil {
utils.WriteError(w, http.StatusBadRequest, "invalid_scope", err.Error())
return
}
canonScope, err := canonicalJSON(dst)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "canon_error", err.Error())
return
}
row.Scope = canonScope
row.ScopeKind = newKind
row.ScopeVersion = newVersion
row.ScopeFingerprint = sha256Hex(canonScope)
}
// Rotate secret
if in.Secret != nil {
// validate against current Provider/Kind/SchemaVersion
def := dto.CredentialRegistry[row.Provider][row.Kind][row.SchemaVersion]
dst := def.New()
if err := json.Unmarshal(*in.Secret, dst); err != nil {
utils.WriteError(w, http.StatusBadRequest, "invalid_secret_json", err.Error())
return
}
if err := def.Validate(dst); err != nil {
utils.WriteError(w, http.StatusBadRequest, "invalid_secret", err.Error())
return
}
canonSecret, err := canonicalJSON(dst)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "canon_error", err.Error())
return
}
cipher, iv, tag, err := utils.EncryptForOrg(orgID, canonSecret, db)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "encrypt_error", err.Error())
return
}
row.EncryptedData = cipher
row.IV = iv
row.Tag = tag
}
if err := db.Save(&row).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
return
}
utils.WriteJSON(w, http.StatusOK, credOut(&row))
}
}
// DeleteCredential godoc
// @ID DeleteCredential
// @Summary Delete credential
// @Tags Credentials
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization ID (UUID)"
// @Param id path string true "Credential ID (UUID)"
// @Success 204
// @Failure 404 {string} string "not found"
// @Router /credentials/{id} [delete]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func DeleteCredential(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
return
}
res := db.Where("organization_id = ? AND id = ?", orgID, id).Delete(&models.Credential{})
if res.Error != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error())
return
}
if res.RowsAffected == 0 {
utils.WriteError(w, http.StatusNotFound, "not_found", "credential not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// RevealCredential godoc
// @ID RevealCredential
// @Summary Reveal decrypted secret (one-time read)
// @Tags Credentials
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization ID (UUID)"
// @Param id path string true "Credential ID (UUID)"
// @Success 200 {object} map[string]any
// @Failure 403 {string} string "organization required"
// @Failure 404 {string} string "not found"
// @Router /credentials/{id}/reveal [post]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func RevealCredential(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.Credential
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", "credential not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
return
}
plain, err := utils.DecryptForOrg(orgID, row.EncryptedData, row.IV, row.Tag, db)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "decrypt_error", err.Error())
return
}
utils.WriteJSON(w, http.StatusOK, plain)
}
}
// -- Helpers
func canonicalJSON(v any) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
var m any
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
return marshalSorted(m)
}
func marshalSorted(v any) ([]byte, error) {
switch vv := v.(type) {
case map[string]any:
keys := make([]string, 0, len(vv))
for k := range vv {
keys = append(keys, k)
}
sort.Strings(keys)
buf := bytes.NewBufferString("{")
for i, k := range keys {
if i > 0 {
buf.WriteByte(',')
}
kb, _ := json.Marshal(k)
buf.Write(kb)
buf.WriteByte(':')
b, err := marshalSorted(vv[k])
if err != nil {
return nil, err
}
buf.Write(b)
}
buf.WriteByte('}')
return buf.Bytes(), nil
case []any:
buf := bytes.NewBufferString("[")
for i, e := range vv {
if i > 0 {
buf.WriteByte(',')
}
b, err := marshalSorted(e)
if err != nil {
return nil, err
}
buf.Write(b)
}
buf.WriteByte(']')
return buf.Bytes(), nil
default:
return json.Marshal(v)
}
}
func sha256Hex(b []byte) string {
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:])
}
// SaveCredentialWithScope validates secret+scope, encrypts, fingerprints, and stores.
func SaveCredentialWithScope(
ctx context.Context,
db *gorm.DB,
orgID uuid.UUID,
provider, kind string,
schemaVersion int,
scopeKind string,
scopeVersion int,
rawScope json.RawMessage,
rawSecret json.RawMessage,
name, accountID, region string,
) (*models.Credential, error) {
// 1) secret shape
pv, ok := dto.CredentialRegistry[provider]
if !ok {
return nil, fmt.Errorf("unknown provider %q", provider)
}
kv, ok := pv[kind]
if !ok {
return nil, fmt.Errorf("unknown kind %q for provider %q", kind, provider)
}
def, ok := kv[schemaVersion]
if !ok {
return nil, fmt.Errorf("unsupported schema version %d for %s/%s", schemaVersion, provider, kind)
}
secretDst := def.New()
if err := json.Unmarshal(rawSecret, secretDst); err != nil {
return nil, fmt.Errorf("payload is not valid JSON for %s/%s: %w", provider, kind, err)
}
if err := def.Validate(secretDst); err != nil {
return nil, fmt.Errorf("invalid %s/%s: %w", provider, kind, err)
}
// 2) scope shape
prScopes, ok := dto.ScopeRegistry[provider]
if !ok {
return nil, fmt.Errorf("no scopes registered for provider %q", provider)
}
kScopes, ok := prScopes[scopeKind]
if !ok {
return nil, fmt.Errorf("invalid scope_kind %q for provider %q", scopeKind, provider)
}
sdef, ok := kScopes[scopeVersion]
if !ok {
return nil, fmt.Errorf("unsupported scope version %d for %s/%s", scopeVersion, provider, scopeKind)
}
scopeDst := sdef.New()
if err := json.Unmarshal(rawScope, scopeDst); err != nil {
return nil, fmt.Errorf("invalid scope JSON: %w", err)
}
if err := sdef.Validate(scopeDst); err != nil {
return nil, fmt.Errorf("invalid scope: %w", err)
}
// 3) canonicalize scope (also what we persist in plaintext)
canonScope, err := canonicalJSON(scopeDst)
if err != nil {
return nil, err
}
fp := sha256Hex(canonScope) // or HMAC if you have a server-side key
// 4) canonicalize + encrypt secret
canonSecret, err := canonicalJSON(secretDst)
if err != nil {
return nil, err
}
cipher, iv, tag, err := utils.EncryptForOrg(orgID, canonSecret, db)
if err != nil {
return nil, fmt.Errorf("encrypt: %w", err)
}
cred := &models.Credential{
OrganizationID: orgID,
Provider: provider,
Kind: kind,
SchemaVersion: schemaVersion,
Name: name,
ScopeKind: scopeKind,
Scope: datatypes.JSON(canonScope),
ScopeVersion: scopeVersion,
AccountID: accountID,
Region: region,
ScopeFingerprint: fp,
EncryptedData: cipher,
IV: iv,
Tag: tag,
}
if err := db.WithContext(ctx).Create(cred).Error; err != nil {
return nil, err
}
return cred, nil
}
// credOut converts model → response DTO
func credOut(c *models.Credential) dto.CredentialOut {
return dto.CredentialOut{
ID: c.ID.String(),
Provider: c.Provider,
Kind: c.Kind,
SchemaVersion: c.SchemaVersion,
Name: c.Name,
ScopeKind: c.ScopeKind,
ScopeVersion: c.ScopeVersion,
Scope: dto.RawJSON(c.Scope),
AccountID: c.AccountID,
Region: c.Region,
CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339),
}
}

View File

@@ -0,0 +1,34 @@
package dto
import (
"time"
"github.com/google/uuid"
)
type ClusterResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Region string `json:"region"`
Status string `json:"status"`
CaptainDomain string `json:"captain_domain"`
ClusterLoadBalancer string `json:"cluster_load_balancer"`
RandomToken string `json:"random_token"`
CertificateKey string `json:"certificate_key"`
ControlLoadBalancer string `json:"control_load_balancer"`
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateClusterRequest struct {
Name string `json:"name"`
Provider string `json:"provider"`
Region string `json:"region"`
Status string `json:"status"`
CaptainDomain string `json:"captain_domain"`
ClusterLoadBalancer *string `json:"cluster_load_balancer"`
ControlLoadBalancer *string `json:"control_load_balancer"`
}

View File

@@ -0,0 +1,138 @@
package dto
import (
"encoding/json"
"github.com/go-playground/validator/v10"
)
// RawJSON is a swagger-friendly wrapper for json.RawMessage.
type RawJSON json.RawMessage
var Validate = validator.New()
func init() {
_ = Validate.RegisterValidation("awsarn", func(fl validator.FieldLevel) bool {
v := fl.Field().String()
return len(v) > 10 && len(v) < 2048 && len(v) >= 4 && v[:4] == "arn:"
})
}
/*** Shapes for secrets ***/
type AWSCredential struct {
AccessKeyID string `json:"access_key_id" validate:"required,alphanum,len=20"`
SecretAccessKey string `json:"secret_access_key" validate:"required"`
Region string `json:"region" validate:"omitempty"`
}
type BasicAuth struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
type APIToken struct {
Token string `json:"token" validate:"required"`
}
type OAuth2Credential struct {
ClientID string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"`
RefreshToken string `json:"refresh_token" validate:"required"`
}
/*** Shapes for scopes ***/
type AWSProviderScope struct{}
type AWSServiceScope struct {
Service string `json:"service" validate:"required,oneof=route53 s3 ec2 iam rds dynamodb"`
}
type AWSResourceScope struct {
ARN string `json:"arn" validate:"required,awsarn"`
}
/*** Registries ***/
type ProviderDef struct {
New func() any
Validate func(any) error
}
type ScopeDef struct {
New func() any
Validate func(any) error
Specificity int // 0=provider, 1=service, 2=resource
}
// Secret shapes per provider/kind/version
var CredentialRegistry = map[string]map[string]map[int]ProviderDef{
"aws": {
"aws_access_key": {
1: {New: func() any { return &AWSCredential{} }, Validate: func(x any) error { return Validate.Struct(x) }},
},
},
"cloudflare": {"api_token": {1: {New: func() any { return &APIToken{} }, Validate: func(x any) error { return Validate.Struct(x) }}}},
"hetzner": {"api_token": {1: {New: func() any { return &APIToken{} }, Validate: func(x any) error { return Validate.Struct(x) }}}},
"digitalocean": {"api_token": {1: {New: func() any { return &APIToken{} }, Validate: func(x any) error { return Validate.Struct(x) }}}},
"generic": {
"basic_auth": {1: {New: func() any { return &BasicAuth{} }, Validate: func(x any) error { return Validate.Struct(x) }}},
"oauth2": {1: {New: func() any { return &OAuth2Credential{} }, Validate: func(x any) error { return Validate.Struct(x) }}},
},
}
// Scope shapes per provider/scopeKind/version
var ScopeRegistry = map[string]map[string]map[int]ScopeDef{
"aws": {
"provider": {1: {New: func() any { return &AWSProviderScope{} }, Validate: func(any) error { return nil }, Specificity: 0}},
"service": {1: {New: func() any { return &AWSServiceScope{} }, Validate: func(x any) error { return Validate.Struct(x) }, Specificity: 1}},
"resource": {1: {New: func() any { return &AWSResourceScope{} }, Validate: func(x any) error { return Validate.Struct(x) }, Specificity: 2}},
},
}
/*** API DTOs used by swagger ***/
// CreateCredentialRequest represents the POST /credentials payload
type CreateCredentialRequest struct {
Provider string `json:"provider" validate:"required,oneof=aws cloudflare hetzner digitalocean generic"`
Kind string `json:"kind" validate:"required"` // aws_access_key, api_token, basic_auth, oauth2
SchemaVersion int `json:"schema_version" validate:"required,gte=1"` // secret schema version
Name string `json:"name" validate:"omitempty,max=100"` // human label
ScopeKind string `json:"scope_kind" validate:"required,oneof=provider service resource"`
ScopeVersion int `json:"scope_version" validate:"required,gte=1"` // scope schema version
Scope RawJSON `json:"scope" validate:"required" swaggertype:"object"` // {"service":"route53"} or {"arn":"..."}
AccountID string `json:"account_id,omitempty" validate:"omitempty,max=32"`
Region string `json:"region,omitempty" validate:"omitempty,max=32"`
Secret RawJSON `json:"secret" validate:"required" swaggertype:"object"` // encrypted later
}
// UpdateCredentialRequest represents PATCH /credentials/{id}
type UpdateCredentialRequest struct {
Name *string `json:"name,omitempty"`
AccountID *string `json:"account_id,omitempty"`
Region *string `json:"region,omitempty"`
ScopeKind *string `json:"scope_kind,omitempty"`
ScopeVersion *int `json:"scope_version,omitempty"`
Scope *RawJSON `json:"scope,omitempty" swaggertype:"object"`
Secret *RawJSON `json:"secret,omitempty" swaggertype:"object"` // set if rotating
}
// CredentialOut is what we return (no secrets)
type CredentialOut struct {
ID string `json:"id"`
Provider string `json:"provider"`
Kind string `json:"kind"`
SchemaVersion int `json:"schema_version"`
Name string `json:"name"`
ScopeKind string `json:"scope_kind"`
ScopeVersion int `json:"scope_version"`
Scope RawJSON `json:"scope" swaggertype:"object"`
AccountID string `json:"account_id,omitempty"`
Region string `json:"region,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

View File

@@ -57,6 +57,6 @@ type PageJob struct {
type EnqueueRequest struct {
Queue string `json:"queue" example:"default"`
Type string `json:"type" example:"email.send"`
Payload json.RawMessage `json:"payload"`
Payload json.RawMessage `json:"payload" swaggertype:"object"`
RunAt *time.Time `json:"run_at" example:"2025-11-05T08:00:00Z"`
}

View File

@@ -16,6 +16,7 @@ type Cluster struct {
Status string `json:"status"`
CaptainDomain string `gorm:"not null" json:"captain_domain"`
ClusterLoadBalancer string `json:"cluster_load_balancer"`
ControlLoadBalancer string `json:"control_load_balancer"`
RandomToken string `json:"random_token"`
CertificateKey string `json:"certificate_key"`
EncryptedKubeconfig string `gorm:"type:text" json:"-"`

View File

@@ -0,0 +1,29 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/datatypes"
)
type Credential struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_credentials_org_provider" json:"organization_id"`
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
Provider string `gorm:"type:varchar(50);not null;index"`
Kind string `gorm:"type:varchar(50);not null;index"` // "aws_access_key", "api_token", "basic_auth", ...
SchemaVersion int `gorm:"not null;default:1"`
Name string `gorm:"type:varchar(100);not null;default:''"` // human label, lets you have multiple for same service
ScopeKind string `gorm:"type:varchar(20);not null"` // "provider" | "service" | "resource"
Scope datatypes.JSON `gorm:"type:jsonb;not null;default:'{}'"` // e.g. {"service":"route53"} or {"arn":"arn:aws:s3:::my-bucket"}
ScopeVersion int `gorm:"not null;default:1"`
AccountID string `gorm:"type:varchar(32)"` // AWS account ID if applicable
Region string `gorm:"type:varchar(32)"` // default region (non-secret)
ScopeFingerprint string `gorm:"type:char(64);not null;index"`
EncryptedData string `gorm:"not null"`
IV string `gorm:"not null"`
Tag string `gorm:"not null"`
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
}

20
internal/models/dns.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Dns struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_credentials_org_provider" json:"organization_id"`
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
ClusterID *uuid.UUID `gorm:"type:uuid" json:"cluster_id,omitempty"`
Cluster *Cluster `gorm:"foreignKey:ClusterID" json:"cluster,omitempty"`
Type string `gorm:"not null" json:"type,omitempty"`
Name string `gorm:"not null" json:"name,omitempty"`
Content string `gorm:"not null" json:"content,omitempty"`
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()"`
}

View File

@@ -12,7 +12,7 @@ type NodePool struct {
Annotations []Annotation `gorm:"many2many:node_annotations;constraint:OnDelete:CASCADE" json:"annotations,omitempty"`
Labels []Label `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"labels,omitempty"`
Taints []Taint `gorm:"many2many:node_taints;constraint:OnDelete:CASCADE" json:"taints,omitempty"`
//Clusters []Cluster `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"clusters,omitempty"`
Clusters []Cluster `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"clusters,omitempty"`
//Topology string `gorm:"not null,default:'stacked'" json:"topology,omitempty"` // stacked or external
Role string `gorm:"not null,default:'worker'" json:"role,omitempty"` // master, worker, or etcd (etcd only if topology = external
}

View File

@@ -4,6 +4,7 @@ import { Route, Routes } from "react-router-dom"
import { ProtectedRoute } from "@/components/protected-route.tsx"
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
import { Login } from "@/pages/auth/login.tsx"
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
import { MePage } from "@/pages/me/me-page.tsx"
@@ -33,6 +34,7 @@ export default function App() {
<Route path="/labels" element={<LabelsPage />} />
<Route path="/annotations" element={<AnnotationPage />} />
<Route path="/node-pools" element={<NodePoolsPage />} />
<Route path="/credentials" element={<CredentialPage />} />
<Route path="/admin/jobs" element={<JobsPage />} />
</Route>

32
ui/src/api/credentials.ts Normal file
View File

@@ -0,0 +1,32 @@
import { withRefresh } from "@/api/with-refresh.ts"
import type { DtoCreateCredentialRequest, DtoUpdateCredentialRequest } from "@/sdk"
import { makeCredentialsApi } from "@/sdkClient.ts"
const credentials = makeCredentialsApi()
export const credentialsApi = {
listCredentials: () =>
withRefresh(async () => {
return await credentials.listCredentials()
}),
createCredential: async (body: DtoCreateCredentialRequest) =>
withRefresh(async () => {
return await credentials.createCredential({ body })
}),
getCredential: async (id: string) =>
withRefresh(async () => {
return await credentials.getCredential({ id })
}),
deleteCredential: async (id: string) =>
withRefresh(async () => {
await credentials.deleteCredential({ id })
}),
updateCredential: async (id: string, body: DtoUpdateCredentialRequest) =>
withRefresh(async () => {
return await credentials.updateCredential({ id, body })
}),
revealCredential: async (id: string) =>
withRefresh(async () => {
return await credentials.revealCredential({ id })
}),
}

View File

@@ -5,6 +5,7 @@ import {
ComponentIcon,
FileKey2Icon,
KeyRound,
LockKeyholeIcon,
ServerIcon,
SprayCanIcon,
TagsIcon,
@@ -28,6 +29,7 @@ export const mainNav: NavItem[] = [
{ to: "/taints", label: "Taints", icon: SprayCanIcon },
{ to: "/servers", label: "Servers", icon: ServerIcon },
{ to: "/ssh", label: "SSH Keys", icon: FileKey2Icon },
{ to: "/credentials", label: "Credentials", icon: LockKeyholeIcon },
]
export const orgNav: NavItem[] = [

View File

@@ -0,0 +1,790 @@
import { useMemo, useState } from "react"
import { credentialsApi } from "@/api/credentials"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Eye, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2 } from "lucide-react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
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 { Textarea } from "@/components/ui/textarea"
// ---------- Schemas ----------
const jsonTransform = z
.string()
.min(2, "JSON required")
.refine((v) => {
try {
JSON.parse(v)
return true
} catch {
return false
}
}, "Invalid JSON")
.transform((v) => JSON.parse(v))
const createCredentialSchema = z.object({
provider: z.enum(["aws", "cloudflare", "hetzner", "digitalocean", "generic"]),
kind: z.enum(["aws_access_key", "api_token", "basic_auth", "oauth2"]),
schema_version: z.number().default(1),
name: z.string().min(1, "Name is required").max(100),
scope_kind: z.enum(["provider", "service", "resource"]),
scope_version: z.number().default(1),
scope: jsonTransform,
account_id: z
.string()
.optional()
.or(z.literal(""))
.transform((v) => (v ? v : undefined)),
region: z
.string()
.optional()
.or(z.literal(""))
.transform((v) => (v ? v : undefined)),
// Secrets are always JSON — makes rotate easy on update form too
secret: jsonTransform,
})
type CreateCredentialInput = z.input<typeof createCredentialSchema>
type CreateCredentialValues = z.infer<typeof createCredentialSchema>
const updateCredentialSchema = createCredentialSchema.partial().extend({
// allow rotating secret independently
secret: jsonTransform.optional(),
name: z.string().min(1, "Name is required").max(100).optional(),
})
// ---------- Helpers ----------
function pretty(obj: unknown) {
try {
return JSON.stringify(obj, null, 2)
} catch {
return ""
}
}
function toFormDefaults<T extends Record<string, any>>(initial: Partial<T>) {
return {
schema_version: 1,
scope_version: 1,
...initial,
} as any
}
// ---------- Page ----------
export const CredentialPage = () => {
const [filter, setFilter] = useState<string>("")
const [createOpen, setCreateOpen] = useState<boolean>(false)
const [editOpen, setEditOpen] = useState<boolean>(false)
const [revealOpen, setRevealOpen] = useState<boolean>(false)
const [revealJson, setRevealJson] = useState<object | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const qc = useQueryClient()
// List
const credentialQ = useQuery({
queryKey: ["credentials"],
queryFn: () => credentialsApi.listCredentials(),
})
// Create
const createMutation = useMutation({
mutationFn: (body: CreateCredentialValues) =>
credentialsApi.createCredential({
provider: body.provider,
kind: body.kind,
schema_version: body.schema_version ?? 1,
name: body.name,
scope_kind: body.scope_kind,
scope_version: body.scope_version ?? 1,
scope: body.scope,
account_id: body.account_id,
region: body.region,
secret: body.secret,
}),
onSuccess: async () => {
await qc.invalidateQueries({ queryKey: ["credentials"] })
toast.success("Credential created")
setCreateOpen(false)
createForm.reset(createDefaults) // clear JSON textareas etc
},
onError: (err: any) => {
toast.error("Failed to create credential", {
description: err?.message ?? "Unknown error",
})
},
})
// Update
const updateMutation = useMutation({
mutationFn: (payload: { id: string; body: z.infer<typeof updateCredentialSchema> }) =>
credentialsApi.updateCredential(payload.id, payload.body),
onSuccess: async () => {
await qc.invalidateQueries({ queryKey: ["credentials"] })
toast.success("Credential updated")
setEditOpen(false)
setEditingId(null)
},
onError: (err: any) => {
toast.error("Failed to update credential", {
description: err?.message ?? "Unknown error",
})
},
})
// Delete
const deleteMutation = useMutation({
mutationFn: (id: string) => credentialsApi.deleteCredential(id),
onSuccess: async () => {
await qc.invalidateQueries({ queryKey: ["credentials"] })
toast.success("Credential deleted")
},
onError: (err: any) => {
toast.error("Failed to delete credential", {
description: err?.message ?? "Unknown error",
})
},
})
// Reveal (one-time read)
const revealMutation = useMutation({
mutationFn: (id: string) => credentialsApi.revealCredential(id),
onSuccess: (data) => {
setRevealJson(data)
setRevealOpen(true)
},
onError: (err: any) => {
toast.error("Failed to reveal secret", {
description: err?.message ?? "Unknown error",
})
},
})
// ---------- Forms ----------
const createDefaults: CreateCredentialInput = toFormDefaults<CreateCredentialInput>({
provider: "aws",
kind: "aws_access_key",
schema_version: 1,
scope_kind: "provider",
scope_version: 1,
name: "",
// IMPORTANT: default valid JSON strings so zod.transform succeeds
scope: "{}" as any,
secret: "{}" as any,
account_id: "",
region: "",
})
const createForm = useForm<CreateCredentialInput>({
resolver: zodResolver(createCredentialSchema),
defaultValues: createDefaults,
mode: "onBlur",
})
const editForm = useForm<z.input<typeof updateCredentialSchema>>({
resolver: zodResolver(updateCredentialSchema),
defaultValues: {
// populated on open
},
mode: "onBlur",
})
function openEdit(row: any) {
setEditingId(row.id)
editForm.reset({
provider: row.provider,
kind: row.kind,
schema_version: row.schema_version ?? 1,
name: row.name,
scope_kind: row.scope_kind,
scope_version: row.scope_version ?? 1,
account_id: row.account_id ?? "",
region: row.region ?? "",
// show JSON in textareas
scope: pretty(row.scope ?? {}),
// secret is optional on update; leave empty to avoid rotate
secret: undefined,
} as any)
setEditOpen(true)
}
const filtered = useMemo(() => {
const items = credentialQ.data ?? []
if (!filter.trim()) return items
const f = filter.toLowerCase()
return items.filter((c: any) =>
[
c.name,
c.provider,
c.kind,
c.scope_kind,
c.account_id,
c.region,
JSON.stringify(c.scope ?? {}),
]
.filter(Boolean)
.map((x: any) => String(x).toLowerCase())
.some((s: string) => s.includes(f))
)
}, [credentialQ.data, filter])
// ---------- UI ----------
if (credentialQ.isLoading)
return (
<div className="flex items-center gap-2 p-6">
<Loader2 className="h-4 w-4 animate-spin" /> Loading credentials
</div>
)
if (credentialQ.error)
return (
<div className="p-6 text-red-500">
Error loading credentials.
<pre className="mt-2 text-xs opacity-80">{JSON.stringify(credentialQ.error, null, 2)}</pre>
</div>
)
return (
<div className="space-y-4 p-6">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<h1 className="mb-1 text-2xl font-bold">Credentials</h1>
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="absolute top-2.5 left-2 h-4 w-4 opacity-60" />
<Input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search by name, provider, kind, scope…"
className="w-64 pl-8"
/>
</div>
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogTrigger asChild>
<Button onClick={() => setCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create Credential
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Create Credential</DialogTitle>
</DialogHeader>
<Form {...createForm}>
<form
onSubmit={createForm.handleSubmit((values) =>
createMutation.mutate(values as CreateCredentialValues)
)}
className="space-y-4 pt-2"
>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<FormField
control={createForm.control}
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>Provider</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="aws">AWS</SelectItem>
<SelectItem value="cloudflare">Cloudflare</SelectItem>
<SelectItem value="hetzner">Hetzner</SelectItem>
<SelectItem value="digitalocean">DigitalOcean</SelectItem>
<SelectItem value="generic">Generic</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="kind"
render={({ field }) => (
<FormItem>
<FormLabel>Kind</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="aws_access_key">AWS Access Key</SelectItem>
<SelectItem value="api_token">API Token</SelectItem>
<SelectItem value="basic_auth">Basic Auth</SelectItem>
<SelectItem value="oauth2">OAuth2</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="scope_kind"
render={({ field }) => (
<FormItem>
<FormLabel>Scope Kind</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="provider">Provider</SelectItem>
<SelectItem value="service">Service</SelectItem>
<SelectItem value="resource">Resource</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<Input {...field} placeholder="My AWS Key" />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="account_id"
render={({ field }) => (
<FormItem>
<FormLabel>Account ID (optional)</FormLabel>
<Input {...field} placeholder="e.g. 123456789012" />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="region"
render={({ field }) => (
<FormItem>
<FormLabel>Region (optional)</FormLabel>
<Input {...field} placeholder="e.g. us-east-1" />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={createForm.control}
name="scope"
render={({ field }) => (
<FormItem>
<FormLabel>Scope (JSON)</FormLabel>
<Textarea
{...field}
rows={3}
placeholder='e.g. {"service":"s3"} or {"arn":"..."}'
className="font-mono"
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="secret"
render={({ field }) => (
<FormItem>
<FormLabel>Secret (JSON)</FormLabel>
<Textarea
{...field}
rows={6}
placeholder='{"access_key_id":"...","secret_access_key":"..."}'
className="font-mono"
/>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="gap-2">
<Button variant="outline" type="button" onClick={() => setCreateOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Create
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto rounded-xl border">
<table className="min-w-full text-sm">
<thead className="bg-muted/40 text-xs tracking-wide uppercase">
<tr>
<th className="w-[28%] px-4 py-2 text-left">Name</th>
<th className="px-4 py-2 text-left">Provider</th>
<th className="px-4 py-2 text-left">Kind</th>
<th className="px-4 py-2 text-left">Scope Kind</th>
<th className="px-4 py-2 text-left">Account</th>
<th className="px-4 py-2 text-left">Region</th>
<th className="px-4 py-2 text-right">Actions</th>
</tr>
</thead>
<tbody>
{filtered.map((row: any) => (
<tr key={row.id} className="border-t">
<td className="px-4 py-2 font-medium">{row.name}</td>
<td className="px-4 py-2">{row.provider}</td>
<td className="px-4 py-2">{row.kind}</td>
<td className="px-4 py-2">{row.scope_kind}</td>
<td className="px-4 py-2">{row.account_id ?? "—"}</td>
<td className="px-4 py-2">{row.region ?? "—"}</td>
<td className="px-4 py-2">
<div className="flex items-center justify-end gap-2">
<Button
size="icon"
variant="ghost"
title="Reveal secret (one-time read)"
onClick={() => revealMutation.mutate(row.id)}
>
<Eye className="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" title="Edit" onClick={() => openEdit(row)}>
<Pencil className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="icon" variant="ghost" title="Delete">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {row.name}?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently remove the credential metadata. Secrets are not
recoverable from the service.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => deleteMutation.mutate(row.id)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openEdit(row)}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => revealMutation.mutate(row.id)}>
Reveal secret
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onClick={() => deleteMutation.mutate(row.id)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</td>
</tr>
))}
{filtered.length === 0 && (
<tr>
<td colSpan={7} className="text-muted-foreground px-4 py-10 text-center">
No credentials match your search.
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Edit dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Edit Credential</DialogTitle>
</DialogHeader>
<Form {...editForm}>
<form
onSubmit={editForm.handleSubmit((values) => {
if (!editingId) return
// Convert stringified JSON fields to objects via schema
const parsed = updateCredentialSchema.safeParse(values)
if (!parsed.success) {
toast.error("Please fix validation errors")
return
}
updateMutation.mutate({ id: editingId, body: parsed.data })
})}
className="space-y-4 pt-2"
>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<FormField
control={editForm.control}
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>Provider</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="aws">AWS</SelectItem>
<SelectItem value="cloudflare">Cloudflare</SelectItem>
<SelectItem value="hetzner">Hetzner</SelectItem>
<SelectItem value="digitalocean">DigitalOcean</SelectItem>
<SelectItem value="generic">Generic</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="kind"
render={({ field }) => (
<FormItem>
<FormLabel>Kind</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="aws_access_key">AWS Access Key</SelectItem>
<SelectItem value="api_token">API Token</SelectItem>
<SelectItem value="basic_auth">Basic Auth</SelectItem>
<SelectItem value="oauth2">OAuth2</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="scope_kind"
render={({ field }) => (
<FormItem>
<FormLabel>Scope Kind</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="provider">Provider</SelectItem>
<SelectItem value="service">Service</SelectItem>
<SelectItem value="resource">Resource</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="account_id"
render={({ field }) => (
<FormItem>
<FormLabel>Account ID</FormLabel>
<Input {...field} placeholder="optional" />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="region"
render={({ field }) => (
<FormItem>
<FormLabel>Region</FormLabel>
<Input {...field} placeholder="optional" />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="scope"
render={({ field }) => (
<FormItem>
<FormLabel>Scope (JSON)</FormLabel>
<Textarea {...field} rows={3} className="font-mono" />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="secret"
render={({ field }) => (
<FormItem>
<FormLabel>Rotate Secret (JSON, optional)</FormLabel>
<Textarea
{...field}
rows={6}
className="font-mono"
placeholder="Leave empty to keep existing secret"
/>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="gap-2">
<Button variant="outline" type="button" onClick={() => setEditOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save changes
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Reveal modal */}
<Dialog open={revealOpen} onOpenChange={setRevealOpen}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Decrypted Secret</DialogTitle>
</DialogHeader>
<div className="bg-muted/40 rounded-lg border p-3">
<pre className="max-h-[50vh] overflow-auto text-xs leading-relaxed">
{pretty(revealJson ?? {})}
</pre>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
navigator.clipboard.writeText(pretty(revealJson ?? {}))
toast.success("Copied to clipboard")
}}
>
Copy
</Button>
<Button onClick={() => setRevealOpen(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -5,18 +5,25 @@ README.md
docs/AnnotationsApi.md
docs/ArcherAdminApi.md
docs/AuthApi.md
docs/ClustersApi.md
docs/CredentialsApi.md
docs/DtoAnnotationResponse.md
docs/DtoAttachAnnotationsRequest.md
docs/DtoAttachLabelsRequest.md
docs/DtoAttachServersRequest.md
docs/DtoAttachTaintsRequest.md
docs/DtoAuthStartResponse.md
docs/DtoClusterResponse.md
docs/DtoCreateAnnotationRequest.md
docs/DtoCreateClusterRequest.md
docs/DtoCreateCredentialRequest.md
docs/DtoCreateLabelRequest.md
docs/DtoCreateNodePoolRequest.md
docs/DtoCreateSSHRequest.md
docs/DtoCreateServerRequest.md
docs/DtoCreateTaintRequest.md
docs/DtoCredentialOut.md
docs/DtoEnqueueRequest.md
docs/DtoJWK.md
docs/DtoJWKS.md
docs/DtoJob.md
@@ -33,6 +40,7 @@ docs/DtoSshRevealResponse.md
docs/DtoTaintResponse.md
docs/DtoTokenPair.md
docs/DtoUpdateAnnotationRequest.md
docs/DtoUpdateCredentialRequest.md
docs/DtoUpdateLabelRequest.md
docs/DtoUpdateNodePoolRequest.md
docs/DtoUpdateServerRequest.md
@@ -68,6 +76,8 @@ package.json
src/apis/AnnotationsApi.ts
src/apis/ArcherAdminApi.ts
src/apis/AuthApi.ts
src/apis/ClustersApi.ts
src/apis/CredentialsApi.ts
src/apis/HealthApi.ts
src/apis/LabelsApi.ts
src/apis/MeAPIKeysApi.ts
@@ -86,12 +96,17 @@ src/models/DtoAttachLabelsRequest.ts
src/models/DtoAttachServersRequest.ts
src/models/DtoAttachTaintsRequest.ts
src/models/DtoAuthStartResponse.ts
src/models/DtoClusterResponse.ts
src/models/DtoCreateAnnotationRequest.ts
src/models/DtoCreateClusterRequest.ts
src/models/DtoCreateCredentialRequest.ts
src/models/DtoCreateLabelRequest.ts
src/models/DtoCreateNodePoolRequest.ts
src/models/DtoCreateSSHRequest.ts
src/models/DtoCreateServerRequest.ts
src/models/DtoCreateTaintRequest.ts
src/models/DtoCredentialOut.ts
src/models/DtoEnqueueRequest.ts
src/models/DtoJWK.ts
src/models/DtoJWKS.ts
src/models/DtoJob.ts
@@ -108,6 +123,7 @@ src/models/DtoSshRevealResponse.ts
src/models/DtoTaintResponse.ts
src/models/DtoTokenPair.ts
src/models/DtoUpdateAnnotationRequest.ts
src/models/DtoUpdateCredentialRequest.ts
src/models/DtoUpdateLabelRequest.ts
src/models/DtoUpdateNodePoolRequest.ts
src/models/DtoUpdateServerRequest.ts

View File

@@ -14,15 +14,15 @@
import * as runtime from "../runtime"
import type { DtoJob, DtoPageJob, DtoQueueInfo, } from "../models/index"
import { DtoJobFromJSON, DtoPageJobFromJSON, DtoQueueInfoFromJSON, } from "../models/index"
import type { DtoEnqueueRequest, DtoJob, DtoPageJob, DtoQueueInfo, } from "../models/index"
import { DtoEnqueueRequestToJSON, DtoJobFromJSON, DtoPageJobFromJSON, DtoQueueInfoFromJSON, } from "../models/index"
export interface AdminCancelArcherJobRequest {
id: string;
}
export interface AdminEnqueueArcherJobRequest {
body: object;
body: DtoEnqueueRequest;
}
export interface AdminListArcherJobsRequest {
@@ -115,7 +115,7 @@ export class ArcherAdminApi extends runtime.BaseAPI {
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: requestParameters['body'] as any,
body: DtoEnqueueRequestToJSON(requestParameters['body']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => DtoJobFromJSON(jsonValue));

View File

@@ -0,0 +1,143 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import * as runtime from "../runtime"
import type { DtoClusterResponse, DtoCreateClusterRequest, } from "../models/index"
import { DtoClusterResponseFromJSON, DtoCreateClusterRequestToJSON, } from "../models/index"
export interface CreateClusterRequest {
body: DtoCreateClusterRequest;
xOrgID?: string;
}
export interface ListClustersRequest {
xOrgID?: string;
q?: string;
}
/**
*
*/
export class ClustersApi extends runtime.BaseAPI {
/**
* Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).
* Create cluster (org scoped)
*/
async createClusterRaw(requestParameters: CreateClusterRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<DtoClusterResponse>> {
if (requestParameters['body'] == null) {
throw new runtime.RequiredError(
'body',
'Required parameter "body" was null or undefined when calling createCluster().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/clusters`;
const response = await this.request({
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: DtoCreateClusterRequestToJSON(requestParameters['body']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => DtoClusterResponseFromJSON(jsonValue));
}
/**
* Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).
* Create cluster (org scoped)
*/
async createCluster(requestParameters: CreateClusterRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DtoClusterResponse> {
const response = await this.createClusterRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).
* List clusters (org scoped)
*/
async listClustersRaw(requestParameters: ListClustersRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<DtoClusterResponse>>> {
const queryParameters: any = {};
if (requestParameters['q'] != null) {
queryParameters['q'] = requestParameters['q'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/clusters`;
const response = await this.request({
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(DtoClusterResponseFromJSON));
}
/**
* Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).
* List clusters (org scoped)
*/
async listClusters(requestParameters: ListClustersRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<DtoClusterResponse>> {
const response = await this.listClustersRaw(requestParameters, initOverrides);
return await response.value();
}
}

View File

@@ -0,0 +1,397 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import * as runtime from "../runtime"
import type { DtoCreateCredentialRequest, DtoCredentialOut, DtoUpdateCredentialRequest, } from "../models/index"
import {
DtoCreateCredentialRequestToJSON,
DtoCredentialOutFromJSON,
DtoUpdateCredentialRequestToJSON,
} from "../models/index"
export interface CreateCredentialRequest {
body: DtoCreateCredentialRequest;
xOrgID?: string;
}
export interface DeleteCredentialRequest {
id: string;
xOrgID?: string;
}
export interface GetCredentialRequest {
id: string;
xOrgID?: string;
}
export interface ListCredentialsRequest {
xOrgID?: string;
provider?: string;
kind?: string;
scopeKind?: string;
}
export interface RevealCredentialRequest {
id: string;
xOrgID?: string;
}
export interface UpdateCredentialRequest {
id: string;
body: DtoUpdateCredentialRequest;
xOrgID?: string;
}
/**
*
*/
export class CredentialsApi extends runtime.BaseAPI {
/**
* Create a credential (encrypts secret)
*/
async createCredentialRaw(requestParameters: CreateCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<DtoCredentialOut>> {
if (requestParameters['body'] == null) {
throw new runtime.RequiredError(
'body',
'Required parameter "body" was null or undefined when calling createCredential().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/credentials`;
const response = await this.request({
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: DtoCreateCredentialRequestToJSON(requestParameters['body']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => DtoCredentialOutFromJSON(jsonValue));
}
/**
* Create a credential (encrypts secret)
*/
async createCredential(requestParameters: CreateCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DtoCredentialOut> {
const response = await this.createCredentialRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Delete credential
*/
async deleteCredentialRaw(requestParameters: DeleteCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling deleteCredential().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/credentials/{id}`;
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id'])));
const response = await this.request({
path: urlPath,
method: 'DELETE',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* Delete credential
*/
async deleteCredential(requestParameters: DeleteCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.deleteCredentialRaw(requestParameters, initOverrides);
}
/**
* Get credential by ID (metadata only)
*/
async getCredentialRaw(requestParameters: GetCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<DtoCredentialOut>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling getCredential().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/credentials/{id}`;
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id'])));
const response = await this.request({
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => DtoCredentialOutFromJSON(jsonValue));
}
/**
* Get credential by ID (metadata only)
*/
async getCredential(requestParameters: GetCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DtoCredentialOut> {
const response = await this.getCredentialRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Returns credential metadata for the current org. Secrets are never returned.
* List credentials (metadata only)
*/
async listCredentialsRaw(requestParameters: ListCredentialsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<DtoCredentialOut>>> {
const queryParameters: any = {};
if (requestParameters['provider'] != null) {
queryParameters['provider'] = requestParameters['provider'];
}
if (requestParameters['kind'] != null) {
queryParameters['kind'] = requestParameters['kind'];
}
if (requestParameters['scopeKind'] != null) {
queryParameters['scope_kind'] = requestParameters['scopeKind'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/credentials`;
const response = await this.request({
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(DtoCredentialOutFromJSON));
}
/**
* Returns credential metadata for the current org. Secrets are never returned.
* List credentials (metadata only)
*/
async listCredentials(requestParameters: ListCredentialsRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<DtoCredentialOut>> {
const response = await this.listCredentialsRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Reveal decrypted secret (one-time read)
*/
async revealCredentialRaw(requestParameters: RevealCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<{ [key: string]: any; }>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling revealCredential().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/credentials/{id}/reveal`;
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id'])));
const response = await this.request({
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse<any>(response);
}
/**
* Reveal decrypted secret (one-time read)
*/
async revealCredential(requestParameters: RevealCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> {
const response = await this.revealCredentialRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Update credential metadata and/or rotate secret
*/
async updateCredentialRaw(requestParameters: UpdateCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<DtoCredentialOut>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling updateCredential().'
);
}
if (requestParameters['body'] == null) {
throw new runtime.RequiredError(
'body',
'Required parameter "body" was null or undefined when calling updateCredential().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (requestParameters['xOrgID'] != null) {
headerParameters['X-Org-ID'] = String(requestParameters['xOrgID']);
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-KEY"] = await this.configuration.apiKey("X-ORG-KEY"); // OrgKeyAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["X-ORG-SECRET"] = await this.configuration.apiKey("X-ORG-SECRET"); // OrgSecretAuth authentication
}
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // BearerAuth authentication
}
let urlPath = `/credentials/{id}`;
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id'])));
const response = await this.request({
path: urlPath,
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: DtoUpdateCredentialRequestToJSON(requestParameters['body']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => DtoCredentialOutFromJSON(jsonValue));
}
/**
* Update credential metadata and/or rotate secret
*/
async updateCredential(requestParameters: UpdateCredentialRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DtoCredentialOut> {
const response = await this.updateCredentialRaw(requestParameters, initOverrides);
return await response.value();
}
}

View File

@@ -3,6 +3,8 @@
export * from './AnnotationsApi';
export * from './ArcherAdminApi';
export * from './AuthApi';
export * from './ClustersApi';
export * from './CredentialsApi';
export * from './HealthApi';
export * from './LabelsApi';
export * from './MeApi';

View File

@@ -113,8 +113,8 @@ async function example() {
const api = new ArcherAdminApi(config);
const body = {
// object | Job parameters
body: Object,
// DtoEnqueueRequest | Job parameters
body: ...,
} satisfies AdminEnqueueArcherJobRequest;
try {
@@ -134,7 +134,7 @@ example().catch(console.error);
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **body** | `object` | Job parameters | |
| **body** | [DtoEnqueueRequest](DtoEnqueueRequest.md) | Job parameters | |
### Return type

View File

@@ -0,0 +1,173 @@
# ClustersApi
All URIs are relative to */api/v1*
| Method | HTTP request | Description |
|------------- | ------------- | -------------|
| [**createCluster**](ClustersApi.md#createcluster) | **POST** /clusters | Create cluster (org scoped) |
| [**listClusters**](ClustersApi.md#listclusters) | **GET** /clusters | List clusters (org scoped) |
## createCluster
> DtoClusterResponse createCluster(body, xOrgID)
Create cluster (org scoped)
Creates a cluster. If &#x60;kubeconfig&#x60; is provided, it will be encrypted per-organization and stored securely (never returned).
### Example
```ts
import {
Configuration,
ClustersApi,
} from '@glueops/autoglue-sdk-go';
import type { CreateClusterRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new ClustersApi(config);
const body = {
// DtoCreateClusterRequest | payload
body: ...,
// string | Organization UUID (optional)
xOrgID: xOrgID_example,
} satisfies CreateClusterRequest;
try {
const data = await api.createCluster(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **body** | [DtoCreateClusterRequest](DtoCreateClusterRequest.md) | payload | |
| **xOrgID** | `string` | Organization UUID | [Optional] [Defaults to `undefined`] |
### Return type
[**DtoClusterResponse**](DtoClusterResponse.md)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: `application/json`
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **201** | Created | - |
| **400** | invalid json | - |
| **401** | Unauthorized | - |
| **403** | organization required | - |
| **500** | create failed | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
## listClusters
> Array&lt;DtoClusterResponse&gt; listClusters(xOrgID, q)
List clusters (org scoped)
Returns clusters for the organization in X-Org-ID. Filter by &#x60;q&#x60; (name contains).
### Example
```ts
import {
Configuration,
ClustersApi,
} from '@glueops/autoglue-sdk-go';
import type { ListClustersRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new ClustersApi(config);
const body = {
// string | Organization UUID (optional)
xOrgID: xOrgID_example,
// string | Name contains (case-insensitive) (optional)
q: q_example,
} satisfies ListClustersRequest;
try {
const data = await api.listClusters(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **xOrgID** | `string` | Organization UUID | [Optional] [Defaults to `undefined`] |
| **q** | `string` | Name contains (case-insensitive) | [Optional] [Defaults to `undefined`] |
### Return type
[**Array&lt;DtoClusterResponse&gt;**](DtoClusterResponse.md)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **200** | OK | - |
| **401** | Unauthorized | - |
| **403** | organization required | - |
| **500** | failed to list clusters | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,495 @@
# CredentialsApi
All URIs are relative to */api/v1*
| Method | HTTP request | Description |
|------------- | ------------- | -------------|
| [**createCredential**](CredentialsApi.md#createcredential) | **POST** /credentials | Create a credential (encrypts secret) |
| [**deleteCredential**](CredentialsApi.md#deletecredential) | **DELETE** /credentials/{id} | Delete credential |
| [**getCredential**](CredentialsApi.md#getcredential) | **GET** /credentials/{id} | Get credential by ID (metadata only) |
| [**listCredentials**](CredentialsApi.md#listcredentials) | **GET** /credentials | List credentials (metadata only) |
| [**revealCredential**](CredentialsApi.md#revealcredential) | **POST** /credentials/{id}/reveal | Reveal decrypted secret (one-time read) |
| [**updateCredential**](CredentialsApi.md#updatecredential) | **PATCH** /credentials/{id} | Update credential metadata and/or rotate secret |
## createCredential
> DtoCredentialOut createCredential(body, xOrgID)
Create a credential (encrypts secret)
### Example
```ts
import {
Configuration,
CredentialsApi,
} from '@glueops/autoglue-sdk-go';
import type { CreateCredentialRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new CredentialsApi(config);
const body = {
// DtoCreateCredentialRequest | Credential payload
body: ...,
// string | Organization ID (UUID) (optional)
xOrgID: xOrgID_example,
} satisfies CreateCredentialRequest;
try {
const data = await api.createCredential(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **body** | [DtoCreateCredentialRequest](DtoCreateCredentialRequest.md) | Credential payload | |
| **xOrgID** | `string` | Organization ID (UUID) | [Optional] [Defaults to `undefined`] |
### Return type
[**DtoCredentialOut**](DtoCredentialOut.md)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: `application/json`
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **201** | Created | - |
| **401** | Unauthorized | - |
| **403** | organization required | - |
| **500** | internal server error | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
## deleteCredential
> deleteCredential(id, xOrgID)
Delete credential
### Example
```ts
import {
Configuration,
CredentialsApi,
} from '@glueops/autoglue-sdk-go';
import type { DeleteCredentialRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new CredentialsApi(config);
const body = {
// string | Credential ID (UUID)
id: id_example,
// string | Organization ID (UUID) (optional)
xOrgID: xOrgID_example,
} satisfies DeleteCredentialRequest;
try {
const data = await api.deleteCredential(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **id** | `string` | Credential ID (UUID) | [Defaults to `undefined`] |
| **xOrgID** | `string` | Organization ID (UUID) | [Optional] [Defaults to `undefined`] |
### Return type
`void` (Empty response body)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **204** | No Content | - |
| **404** | not found | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
## getCredential
> DtoCredentialOut getCredential(id, xOrgID)
Get credential by ID (metadata only)
### Example
```ts
import {
Configuration,
CredentialsApi,
} from '@glueops/autoglue-sdk-go';
import type { GetCredentialRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new CredentialsApi(config);
const body = {
// string | Credential ID (UUID)
id: id_example,
// string | Organization ID (UUID) (optional)
xOrgID: xOrgID_example,
} satisfies GetCredentialRequest;
try {
const data = await api.getCredential(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **id** | `string` | Credential ID (UUID) | [Defaults to `undefined`] |
| **xOrgID** | `string` | Organization ID (UUID) | [Optional] [Defaults to `undefined`] |
### Return type
[**DtoCredentialOut**](DtoCredentialOut.md)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **200** | OK | - |
| **401** | Unauthorized | - |
| **403** | organization required | - |
| **500** | internal server error | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
## listCredentials
> Array&lt;DtoCredentialOut&gt; listCredentials(xOrgID, provider, kind, scopeKind)
List credentials (metadata only)
Returns credential metadata for the current org. Secrets are never returned.
### Example
```ts
import {
Configuration,
CredentialsApi,
} from '@glueops/autoglue-sdk-go';
import type { ListCredentialsRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new CredentialsApi(config);
const body = {
// string | Organization ID (UUID) (optional)
xOrgID: xOrgID_example,
// string | Filter by provider (e.g., aws) (optional)
provider: provider_example,
// string | Filter by kind (e.g., aws_access_key) (optional)
kind: kind_example,
// string | Filter by scope kind (provider/service/resource) (optional)
scopeKind: scopeKind_example,
} satisfies ListCredentialsRequest;
try {
const data = await api.listCredentials(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **xOrgID** | `string` | Organization ID (UUID) | [Optional] [Defaults to `undefined`] |
| **provider** | `string` | Filter by provider (e.g., aws) | [Optional] [Defaults to `undefined`] |
| **kind** | `string` | Filter by kind (e.g., aws_access_key) | [Optional] [Defaults to `undefined`] |
| **scopeKind** | `string` | Filter by scope kind (provider/service/resource) | [Optional] [Defaults to `undefined`] |
### Return type
[**Array&lt;DtoCredentialOut&gt;**](DtoCredentialOut.md)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **200** | OK | - |
| **401** | Unauthorized | - |
| **403** | organization required | - |
| **500** | internal server error | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
## revealCredential
> { [key: string]: any; } revealCredential(id, xOrgID)
Reveal decrypted secret (one-time read)
### Example
```ts
import {
Configuration,
CredentialsApi,
} from '@glueops/autoglue-sdk-go';
import type { RevealCredentialRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new CredentialsApi(config);
const body = {
// string | Credential ID (UUID)
id: id_example,
// string | Organization ID (UUID) (optional)
xOrgID: xOrgID_example,
} satisfies RevealCredentialRequest;
try {
const data = await api.revealCredential(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **id** | `string` | Credential ID (UUID) | [Defaults to `undefined`] |
| **xOrgID** | `string` | Organization ID (UUID) | [Optional] [Defaults to `undefined`] |
### Return type
**{ [key: string]: any; }**
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **200** | OK | - |
| **403** | organization required | - |
| **404** | not found | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
## updateCredential
> DtoCredentialOut updateCredential(id, body, xOrgID)
Update credential metadata and/or rotate secret
### Example
```ts
import {
Configuration,
CredentialsApi,
} from '@glueops/autoglue-sdk-go';
import type { UpdateCredentialRequest } from '@glueops/autoglue-sdk-go';
async function example() {
console.log("🚀 Testing @glueops/autoglue-sdk-go SDK...");
const config = new Configuration({
// To configure API key authorization: OrgKeyAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: OrgSecretAuth
apiKey: "YOUR API KEY",
// To configure API key authorization: BearerAuth
apiKey: "YOUR API KEY",
});
const api = new CredentialsApi(config);
const body = {
// string | Credential ID (UUID)
id: id_example,
// DtoUpdateCredentialRequest | Fields to update
body: ...,
// string | Organization ID (UUID) (optional)
xOrgID: xOrgID_example,
} satisfies UpdateCredentialRequest;
try {
const data = await api.updateCredential(body);
console.log(data);
} catch (error) {
console.error(error);
}
}
// Run the test
example().catch(console.error);
```
### Parameters
| Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
| **id** | `string` | Credential ID (UUID) | [Defaults to `undefined`] |
| **body** | [DtoUpdateCredentialRequest](DtoUpdateCredentialRequest.md) | Fields to update | |
| **xOrgID** | `string` | Organization ID (UUID) | [Optional] [Defaults to `undefined`] |
### Return type
[**DtoCredentialOut**](DtoCredentialOut.md)
### Authorization
[OrgKeyAuth](../README.md#OrgKeyAuth), [OrgSecretAuth](../README.md#OrgSecretAuth), [BearerAuth](../README.md#BearerAuth)
### HTTP request headers
- **Content-Type**: `application/json`
- **Accept**: `application/json`
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **200** | OK | - |
| **403** | X-Org-ID required | - |
| **404** | not found | - |
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,60 @@
# DtoClusterResponse
## Properties
Name | Type
------------ | -------------
`bastion_server` | [DtoServerResponse](DtoServerResponse.md)
`captain_domain` | string
`certificate_key` | string
`cluster_load_balancer` | string
`control_load_balancer` | string
`created_at` | string
`id` | string
`name` | string
`node_pools` | [Array&lt;DtoNodePoolResponse&gt;](DtoNodePoolResponse.md)
`provider` | string
`random_token` | string
`region` | string
`status` | string
`updated_at` | string
## Example
```typescript
import type { DtoClusterResponse } from '@glueops/autoglue-sdk-go'
// TODO: Update the object below with actual values
const example = {
"bastion_server": null,
"captain_domain": null,
"certificate_key": null,
"cluster_load_balancer": null,
"control_load_balancer": null,
"created_at": null,
"id": null,
"name": null,
"node_pools": null,
"provider": null,
"random_token": null,
"region": null,
"status": null,
"updated_at": null,
} satisfies DtoClusterResponse
console.log(example)
// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)
// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DtoClusterResponse
console.log(exampleParsed)
```
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,46 @@
# DtoCreateClusterRequest
## Properties
Name | Type
------------ | -------------
`captain_domain` | string
`cluster_load_balancer` | string
`control_load_balancer` | string
`name` | string
`provider` | string
`region` | string
`status` | string
## Example
```typescript
import type { DtoCreateClusterRequest } from '@glueops/autoglue-sdk-go'
// TODO: Update the object below with actual values
const example = {
"captain_domain": null,
"cluster_load_balancer": null,
"control_load_balancer": null,
"name": null,
"provider": null,
"region": null,
"status": null,
} satisfies DtoCreateClusterRequest
console.log(example)
// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)
// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DtoCreateClusterRequest
console.log(exampleParsed)
```
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,52 @@
# DtoCreateCredentialRequest
## Properties
Name | Type
------------ | -------------
`account_id` | string
`kind` | string
`name` | string
`provider` | string
`region` | string
`schema_version` | number
`scope` | object
`scope_kind` | string
`scope_version` | number
`secret` | object
## Example
```typescript
import type { DtoCreateCredentialRequest } from '@glueops/autoglue-sdk-go'
// TODO: Update the object below with actual values
const example = {
"account_id": null,
"kind": null,
"name": null,
"provider": null,
"region": null,
"schema_version": null,
"scope": null,
"scope_kind": null,
"scope_version": null,
"secret": null,
} satisfies DtoCreateCredentialRequest
console.log(example)
// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)
// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DtoCreateCredentialRequest
console.log(exampleParsed)
```
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,56 @@
# DtoCredentialOut
## Properties
Name | Type
------------ | -------------
`account_id` | string
`created_at` | string
`id` | string
`kind` | string
`name` | string
`provider` | string
`region` | string
`schema_version` | number
`scope` | object
`scope_kind` | string
`scope_version` | number
`updated_at` | string
## Example
```typescript
import type { DtoCredentialOut } from '@glueops/autoglue-sdk-go'
// TODO: Update the object below with actual values
const example = {
"account_id": null,
"created_at": null,
"id": null,
"kind": null,
"name": null,
"provider": null,
"region": null,
"schema_version": null,
"scope": null,
"scope_kind": null,
"scope_version": null,
"updated_at": null,
} satisfies DtoCredentialOut
console.log(example)
// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)
// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DtoCredentialOut
console.log(exampleParsed)
```
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,40 @@
# DtoEnqueueRequest
## Properties
Name | Type
------------ | -------------
`payload` | object
`queue` | string
`run_at` | string
`type` | string
## Example
```typescript
import type { DtoEnqueueRequest } from '@glueops/autoglue-sdk-go'
// TODO: Update the object below with actual values
const example = {
"payload": null,
"queue": default,
"run_at": 2025-11-05T08:00:00Z,
"type": email.send,
} satisfies DtoEnqueueRequest
console.log(example)
// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)
// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DtoEnqueueRequest
console.log(exampleParsed)
```
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,46 @@
# DtoUpdateCredentialRequest
## Properties
Name | Type
------------ | -------------
`account_id` | string
`name` | string
`region` | string
`scope` | object
`scope_kind` | string
`scope_version` | number
`secret` | object
## Example
```typescript
import type { DtoUpdateCredentialRequest } from '@glueops/autoglue-sdk-go'
// TODO: Update the object below with actual values
const example = {
"account_id": null,
"name": null,
"region": null,
"scope": null,
"scope_kind": null,
"scope_version": null,
"secret": null,
} satisfies DtoUpdateCredentialRequest
console.log(example)
// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)
// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DtoUpdateCredentialRequest
console.log(exampleParsed)
```
[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,173 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { DtoNodePoolResponse } from "./DtoNodePoolResponse"
import { DtoNodePoolResponseFromJSON, DtoNodePoolResponseToJSON, } from "./DtoNodePoolResponse"
import type { DtoServerResponse } from "./DtoServerResponse"
import { DtoServerResponseFromJSON, DtoServerResponseToJSON, } from "./DtoServerResponse"
/**
*
* @export
* @interface DtoClusterResponse
*/
export interface DtoClusterResponse {
/**
*
* @type {DtoServerResponse}
* @memberof DtoClusterResponse
*/
bastion_server?: DtoServerResponse;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
captain_domain?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
certificate_key?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
cluster_load_balancer?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
control_load_balancer?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
created_at?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
id?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
name?: string;
/**
*
* @type {Array<DtoNodePoolResponse>}
* @memberof DtoClusterResponse
*/
node_pools?: Array<DtoNodePoolResponse>;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
provider?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
random_token?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
region?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
status?: string;
/**
*
* @type {string}
* @memberof DtoClusterResponse
*/
updated_at?: string;
}
/**
* Check if a given object implements the DtoClusterResponse interface.
*/
export function instanceOfDtoClusterResponse(value: object): value is DtoClusterResponse {
return true;
}
export function DtoClusterResponseFromJSON(json: any): DtoClusterResponse {
return DtoClusterResponseFromJSONTyped(json, false);
}
export function DtoClusterResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): DtoClusterResponse {
if (json == null) {
return json;
}
return {
'bastion_server': json['bastion_server'] == null ? undefined : DtoServerResponseFromJSON(json['bastion_server']),
'captain_domain': json['captain_domain'] == null ? undefined : json['captain_domain'],
'certificate_key': json['certificate_key'] == null ? undefined : json['certificate_key'],
'cluster_load_balancer': json['cluster_load_balancer'] == null ? undefined : json['cluster_load_balancer'],
'control_load_balancer': json['control_load_balancer'] == null ? undefined : json['control_load_balancer'],
'created_at': json['created_at'] == null ? undefined : json['created_at'],
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'node_pools': json['node_pools'] == null ? undefined : ((json['node_pools'] as Array<any>).map(DtoNodePoolResponseFromJSON)),
'provider': json['provider'] == null ? undefined : json['provider'],
'random_token': json['random_token'] == null ? undefined : json['random_token'],
'region': json['region'] == null ? undefined : json['region'],
'status': json['status'] == null ? undefined : json['status'],
'updated_at': json['updated_at'] == null ? undefined : json['updated_at'],
};
}
export function DtoClusterResponseToJSON(json: any): DtoClusterResponse {
return DtoClusterResponseToJSONTyped(json, false);
}
export function DtoClusterResponseToJSONTyped(value?: DtoClusterResponse | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'bastion_server': DtoServerResponseToJSON(value['bastion_server']),
'captain_domain': value['captain_domain'],
'certificate_key': value['certificate_key'],
'cluster_load_balancer': value['cluster_load_balancer'],
'control_load_balancer': value['control_load_balancer'],
'created_at': value['created_at'],
'id': value['id'],
'name': value['name'],
'node_pools': value['node_pools'] == null ? undefined : ((value['node_pools'] as Array<any>).map(DtoNodePoolResponseToJSON)),
'provider': value['provider'],
'random_token': value['random_token'],
'region': value['region'],
'status': value['status'],
'updated_at': value['updated_at'],
};
}

View File

@@ -0,0 +1,112 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
* @interface DtoCreateClusterRequest
*/
export interface DtoCreateClusterRequest {
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
captain_domain?: string;
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
cluster_load_balancer?: string;
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
control_load_balancer?: string;
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
name?: string;
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
provider?: string;
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
region?: string;
/**
*
* @type {string}
* @memberof DtoCreateClusterRequest
*/
status?: string;
}
/**
* Check if a given object implements the DtoCreateClusterRequest interface.
*/
export function instanceOfDtoCreateClusterRequest(value: object): value is DtoCreateClusterRequest {
return true;
}
export function DtoCreateClusterRequestFromJSON(json: any): DtoCreateClusterRequest {
return DtoCreateClusterRequestFromJSONTyped(json, false);
}
export function DtoCreateClusterRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): DtoCreateClusterRequest {
if (json == null) {
return json;
}
return {
'captain_domain': json['captain_domain'] == null ? undefined : json['captain_domain'],
'cluster_load_balancer': json['cluster_load_balancer'] == null ? undefined : json['cluster_load_balancer'],
'control_load_balancer': json['control_load_balancer'] == null ? undefined : json['control_load_balancer'],
'name': json['name'] == null ? undefined : json['name'],
'provider': json['provider'] == null ? undefined : json['provider'],
'region': json['region'] == null ? undefined : json['region'],
'status': json['status'] == null ? undefined : json['status'],
};
}
export function DtoCreateClusterRequestToJSON(json: any): DtoCreateClusterRequest {
return DtoCreateClusterRequestToJSONTyped(json, false);
}
export function DtoCreateClusterRequestToJSONTyped(value?: DtoCreateClusterRequest | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'captain_domain': value['captain_domain'],
'cluster_load_balancer': value['cluster_load_balancer'],
'control_load_balancer': value['control_load_balancer'],
'name': value['name'],
'provider': value['provider'],
'region': value['region'],
'status': value['status'],
};
}

View File

@@ -0,0 +1,167 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
* @interface DtoCreateCredentialRequest
*/
export interface DtoCreateCredentialRequest {
/**
*
* @type {string}
* @memberof DtoCreateCredentialRequest
*/
account_id?: string;
/**
* aws_access_key, api_token, basic_auth, oauth2
* @type {string}
* @memberof DtoCreateCredentialRequest
*/
kind: string;
/**
* human label
* @type {string}
* @memberof DtoCreateCredentialRequest
*/
name?: string;
/**
*
* @type {string}
* @memberof DtoCreateCredentialRequest
*/
provider: DtoCreateCredentialRequestProviderEnum;
/**
*
* @type {string}
* @memberof DtoCreateCredentialRequest
*/
region?: string;
/**
* secret schema version
* @type {number}
* @memberof DtoCreateCredentialRequest
*/
schema_version: number;
/**
* {"service":"route53"} or {"arn":"..."}
* @type {object}
* @memberof DtoCreateCredentialRequest
*/
scope: object;
/**
*
* @type {string}
* @memberof DtoCreateCredentialRequest
*/
scope_kind: DtoCreateCredentialRequestScopeKindEnum;
/**
* scope schema version
* @type {number}
* @memberof DtoCreateCredentialRequest
*/
scope_version: number;
/**
* encrypted later
* @type {object}
* @memberof DtoCreateCredentialRequest
*/
secret: object;
}
/**
* @export
*/
export const DtoCreateCredentialRequestProviderEnum = {
aws: 'aws',
cloudflare: 'cloudflare',
hetzner: 'hetzner',
digitalocean: 'digitalocean',
generic: 'generic'
} as const;
export type DtoCreateCredentialRequestProviderEnum = typeof DtoCreateCredentialRequestProviderEnum[keyof typeof DtoCreateCredentialRequestProviderEnum];
/**
* @export
*/
export const DtoCreateCredentialRequestScopeKindEnum = {
provider: 'provider',
service: 'service',
resource: 'resource'
} as const;
export type DtoCreateCredentialRequestScopeKindEnum = typeof DtoCreateCredentialRequestScopeKindEnum[keyof typeof DtoCreateCredentialRequestScopeKindEnum];
/**
* Check if a given object implements the DtoCreateCredentialRequest interface.
*/
export function instanceOfDtoCreateCredentialRequest(value: object): value is DtoCreateCredentialRequest {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('provider' in value) || value['provider'] === undefined) return false;
if (!('schema_version' in value) || value['schema_version'] === undefined) return false;
if (!('scope' in value) || value['scope'] === undefined) return false;
if (!('scope_kind' in value) || value['scope_kind'] === undefined) return false;
if (!('scope_version' in value) || value['scope_version'] === undefined) return false;
if (!('secret' in value) || value['secret'] === undefined) return false;
return true;
}
export function DtoCreateCredentialRequestFromJSON(json: any): DtoCreateCredentialRequest {
return DtoCreateCredentialRequestFromJSONTyped(json, false);
}
export function DtoCreateCredentialRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): DtoCreateCredentialRequest {
if (json == null) {
return json;
}
return {
'account_id': json['account_id'] == null ? undefined : json['account_id'],
'kind': json['kind'],
'name': json['name'] == null ? undefined : json['name'],
'provider': json['provider'],
'region': json['region'] == null ? undefined : json['region'],
'schema_version': json['schema_version'],
'scope': json['scope'],
'scope_kind': json['scope_kind'],
'scope_version': json['scope_version'],
'secret': json['secret'],
};
}
export function DtoCreateCredentialRequestToJSON(json: any): DtoCreateCredentialRequest {
return DtoCreateCredentialRequestToJSONTyped(json, false);
}
export function DtoCreateCredentialRequestToJSONTyped(value?: DtoCreateCredentialRequest | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'account_id': value['account_id'],
'kind': value['kind'],
'name': value['name'],
'provider': value['provider'],
'region': value['region'],
'schema_version': value['schema_version'],
'scope': value['scope'],
'scope_kind': value['scope_kind'],
'scope_version': value['scope_version'],
'secret': value['secret'],
};
}

View File

@@ -0,0 +1,152 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
* @interface DtoCredentialOut
*/
export interface DtoCredentialOut {
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
account_id?: string;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
created_at?: string;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
id?: string;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
kind?: string;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
name?: string;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
provider?: string;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
region?: string;
/**
*
* @type {number}
* @memberof DtoCredentialOut
*/
schema_version?: number;
/**
*
* @type {object}
* @memberof DtoCredentialOut
*/
scope?: object;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
scope_kind?: string;
/**
*
* @type {number}
* @memberof DtoCredentialOut
*/
scope_version?: number;
/**
*
* @type {string}
* @memberof DtoCredentialOut
*/
updated_at?: string;
}
/**
* Check if a given object implements the DtoCredentialOut interface.
*/
export function instanceOfDtoCredentialOut(value: object): value is DtoCredentialOut {
return true;
}
export function DtoCredentialOutFromJSON(json: any): DtoCredentialOut {
return DtoCredentialOutFromJSONTyped(json, false);
}
export function DtoCredentialOutFromJSONTyped(json: any, ignoreDiscriminator: boolean): DtoCredentialOut {
if (json == null) {
return json;
}
return {
'account_id': json['account_id'] == null ? undefined : json['account_id'],
'created_at': json['created_at'] == null ? undefined : json['created_at'],
'id': json['id'] == null ? undefined : json['id'],
'kind': json['kind'] == null ? undefined : json['kind'],
'name': json['name'] == null ? undefined : json['name'],
'provider': json['provider'] == null ? undefined : json['provider'],
'region': json['region'] == null ? undefined : json['region'],
'schema_version': json['schema_version'] == null ? undefined : json['schema_version'],
'scope': json['scope'] == null ? undefined : json['scope'],
'scope_kind': json['scope_kind'] == null ? undefined : json['scope_kind'],
'scope_version': json['scope_version'] == null ? undefined : json['scope_version'],
'updated_at': json['updated_at'] == null ? undefined : json['updated_at'],
};
}
export function DtoCredentialOutToJSON(json: any): DtoCredentialOut {
return DtoCredentialOutToJSONTyped(json, false);
}
export function DtoCredentialOutToJSONTyped(value?: DtoCredentialOut | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'account_id': value['account_id'],
'created_at': value['created_at'],
'id': value['id'],
'kind': value['kind'],
'name': value['name'],
'provider': value['provider'],
'region': value['region'],
'schema_version': value['schema_version'],
'scope': value['scope'],
'scope_kind': value['scope_kind'],
'scope_version': value['scope_version'],
'updated_at': value['updated_at'],
};
}

View File

@@ -0,0 +1,88 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
* @interface DtoEnqueueRequest
*/
export interface DtoEnqueueRequest {
/**
*
* @type {object}
* @memberof DtoEnqueueRequest
*/
payload?: object;
/**
*
* @type {string}
* @memberof DtoEnqueueRequest
*/
queue?: string;
/**
*
* @type {string}
* @memberof DtoEnqueueRequest
*/
run_at?: string;
/**
*
* @type {string}
* @memberof DtoEnqueueRequest
*/
type?: string;
}
/**
* Check if a given object implements the DtoEnqueueRequest interface.
*/
export function instanceOfDtoEnqueueRequest(value: object): value is DtoEnqueueRequest {
return true;
}
export function DtoEnqueueRequestFromJSON(json: any): DtoEnqueueRequest {
return DtoEnqueueRequestFromJSONTyped(json, false);
}
export function DtoEnqueueRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): DtoEnqueueRequest {
if (json == null) {
return json;
}
return {
'payload': json['payload'] == null ? undefined : json['payload'],
'queue': json['queue'] == null ? undefined : json['queue'],
'run_at': json['run_at'] == null ? undefined : json['run_at'],
'type': json['type'] == null ? undefined : json['type'],
};
}
export function DtoEnqueueRequestToJSON(json: any): DtoEnqueueRequest {
return DtoEnqueueRequestToJSONTyped(json, false);
}
export function DtoEnqueueRequestToJSONTyped(value?: DtoEnqueueRequest | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'payload': value['payload'],
'queue': value['queue'],
'run_at': value['run_at'],
'type': value['type'],
};
}

View File

@@ -0,0 +1,112 @@
/* tslint:disable */
/* eslint-disable */
/**
* AutoGlue API
* API for managing K3s clusters across cloud providers
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
* @interface DtoUpdateCredentialRequest
*/
export interface DtoUpdateCredentialRequest {
/**
*
* @type {string}
* @memberof DtoUpdateCredentialRequest
*/
account_id?: string;
/**
*
* @type {string}
* @memberof DtoUpdateCredentialRequest
*/
name?: string;
/**
*
* @type {string}
* @memberof DtoUpdateCredentialRequest
*/
region?: string;
/**
*
* @type {object}
* @memberof DtoUpdateCredentialRequest
*/
scope?: object;
/**
*
* @type {string}
* @memberof DtoUpdateCredentialRequest
*/
scope_kind?: string;
/**
*
* @type {number}
* @memberof DtoUpdateCredentialRequest
*/
scope_version?: number;
/**
* set if rotating
* @type {object}
* @memberof DtoUpdateCredentialRequest
*/
secret?: object;
}
/**
* Check if a given object implements the DtoUpdateCredentialRequest interface.
*/
export function instanceOfDtoUpdateCredentialRequest(value: object): value is DtoUpdateCredentialRequest {
return true;
}
export function DtoUpdateCredentialRequestFromJSON(json: any): DtoUpdateCredentialRequest {
return DtoUpdateCredentialRequestFromJSONTyped(json, false);
}
export function DtoUpdateCredentialRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): DtoUpdateCredentialRequest {
if (json == null) {
return json;
}
return {
'account_id': json['account_id'] == null ? undefined : json['account_id'],
'name': json['name'] == null ? undefined : json['name'],
'region': json['region'] == null ? undefined : json['region'],
'scope': json['scope'] == null ? undefined : json['scope'],
'scope_kind': json['scope_kind'] == null ? undefined : json['scope_kind'],
'scope_version': json['scope_version'] == null ? undefined : json['scope_version'],
'secret': json['secret'] == null ? undefined : json['secret'],
};
}
export function DtoUpdateCredentialRequestToJSON(json: any): DtoUpdateCredentialRequest {
return DtoUpdateCredentialRequestToJSONTyped(json, false);
}
export function DtoUpdateCredentialRequestToJSONTyped(value?: DtoUpdateCredentialRequest | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'account_id': value['account_id'],
'name': value['name'],
'region': value['region'],
'scope': value['scope'],
'scope_kind': value['scope_kind'],
'scope_version': value['scope_version'],
'secret': value['secret'],
};
}

View File

@@ -6,12 +6,17 @@ export * from './DtoAttachLabelsRequest';
export * from './DtoAttachServersRequest';
export * from './DtoAttachTaintsRequest';
export * from './DtoAuthStartResponse';
export * from './DtoClusterResponse';
export * from './DtoCreateAnnotationRequest';
export * from './DtoCreateClusterRequest';
export * from './DtoCreateCredentialRequest';
export * from './DtoCreateLabelRequest';
export * from './DtoCreateNodePoolRequest';
export * from './DtoCreateSSHRequest';
export * from './DtoCreateServerRequest';
export * from './DtoCreateTaintRequest';
export * from './DtoCredentialOut';
export * from './DtoEnqueueRequest';
export * from './DtoJWK';
export * from './DtoJWKS';
export * from './DtoJob';
@@ -28,6 +33,7 @@ export * from './DtoSshRevealResponse';
export * from './DtoTaintResponse';
export * from './DtoTokenPair';
export * from './DtoUpdateAnnotationRequest';
export * from './DtoUpdateCredentialRequest';
export * from './DtoUpdateLabelRequest';
export * from './DtoUpdateNodePoolRequest';
export * from './DtoUpdateServerRequest';

View File

@@ -5,6 +5,7 @@ import {
ArcherAdminApi,
AuthApi,
Configuration,
CredentialsApi,
LabelsApi,
MeApi,
MeAPIKeysApi,
@@ -113,3 +114,7 @@ export function makeNodePoolApi() {
export function makeMetaApi() {
return makeApiClient(MetaApi)
}
export function makeCredentialsApi() {
return makeApiClient(CredentialsApi)
}