Compare commits

...

24 Commits

Author SHA1 Message Date
allanice001
842f7c9be6 fix: bugfix jobs
Signed-off-by: allanice001 <allanice001@gmail.com>
2025-12-16 00:52:16 +00:00
public-glueops-renovatebot[bot]
c15311a5a1 chore(patch): update @eslint/js to 9.39.2 #patch (#456)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 22:26:54 +00:00
public-glueops-renovatebot[bot]
25ced343c4 feat: update shadcn to 3.6.0 #minor (#454)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 20:23:28 +00:00
public-glueops-renovatebot[bot]
b72a8d384d feat: update github.com/aws/aws-sdk-go-v2/service/s3 to v1.94.0 #minor (#470)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 20:22:48 +00:00
public-glueops-renovatebot[bot]
c786a79b60 chore: lock file maintenance (#469)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 19:39:22 +00:00
public-glueops-renovatebot[bot]
01b1434842 chore: lock file maintenance (#468)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 19:38:13 +00:00
public-glueops-renovatebot[bot]
e8c9cde474 chore: lock file maintenance (#467)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 19:09:53 +00:00
public-glueops-renovatebot[bot]
ae92d05cd4 feat: update typescript-eslint to 8.50.0 #minor (#465)
* chore: lock file maintenance (#452)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* feat: update github.com/go-playground/validator/v10 to v10.29.0 #minor (#453)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* chore: lock file maintenance (#455)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* chore: lock file maintenance (#457)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* chore: lock file maintenance (#462)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* chore: lock file maintenance (#464)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* chore: lock file maintenance (#466)

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>

* feat: update typescript-eslint to 8.50.0 #minor

---------

Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-15 18:08:04 +00:00
allanice001
67d50d2b15 fix: bugfix in responding with correct label ids 2025-12-15 18:04:22 +00:00
allanice001
e5a664b812 Merge remote-tracking branch 'origin/main' 2025-12-12 11:36:33 +00:00
allanice001
f722ba8dca chore: update dependencies
Signed-off-by: allanice001 <allanice001@gmail.com>
2025-12-12 11:36:25 +00:00
public-glueops-renovatebot[bot]
20e6d8d186 chore: lock file maintenance (#449)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-12 07:40:12 +00:00
allanice001
85f37cd113 fix: ui updates for org api keys
Signed-off-by: allanice001 <allanice001@gmail.com>
2025-12-12 02:05:31 +00:00
allanice001
fd1a81ecd8 fix: api keys form bugfix and org key sweeper job
Signed-off-by: allanice001 <allanice001@gmail.com>
2025-12-12 01:37:42 +00:00
allanice001
793daf3ac3 Merge remote-tracking branch 'origin/main' 2025-12-12 00:20:34 +00:00
allanice001
7bef4ef6f1 feat: add org_key and org_secret to payload on bastion
Signed-off-by: allanice001 <allanice001@gmail.com>
2025-12-12 00:20:27 +00:00
public-glueops-renovatebot[bot]
9fa9cd169b chore: lock file maintenance (#448)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-11 23:26:33 +00:00
public-glueops-renovatebot[bot]
8812b43346 chore: lock file maintenance (#447)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-11 20:39:59 +00:00
public-glueops-renovatebot[bot]
21a6d7d5a1 chore: lock file maintenance (#445)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-11 19:37:53 +00:00
public-glueops-renovatebot[bot]
da332c89dd chore: lock file maintenance (#443)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-11 17:12:09 +00:00
public-glueops-renovatebot[bot]
fd25825f34 chore: lock file maintenance (#441)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-11 15:54:18 +00:00
allanice001
de3740e974 Merge remote-tracking branch 'origin/main' 2025-12-11 13:08:47 +00:00
allanice001
21dd26503f feat: add kubeconfig to payload if available
Signed-off-by: allanice001 <allanice001@gmail.com>
2025-12-11 13:08:39 +00:00
public-glueops-renovatebot[bot]
e1da229c30 chore: lock file maintenance (#439)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-12-10 14:43:04 +00:00
19 changed files with 825 additions and 383 deletions

View File

@@ -156,6 +156,21 @@ var serveCmd = &cobra.Command{
if err != nil {
log.Printf("failed to enqueue cluster bootstrap: %v", err)
}
_, err = jobs.Enqueue(
context.Background(),
uuid.NewString(),
"org_key_sweeper",
bg.OrgKeySweeperArgs{
IntervalS: 3600,
RetentionDays: 10,
},
archer.WithMaxRetries(1),
archer.WithScheduleTime(time.Now()),
)
if err != nil {
log.Printf("failed to enqueue org_key_sweeper: %v", err)
}
}
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -104,6 +104,8 @@ components:
$ref: '#/components/schemas/dto.LoadBalancerResponse'
id:
type: string
kubeconfig:
type: string
last_error:
type: string
name:
@@ -113,6 +115,10 @@ components:
$ref: '#/components/schemas/dto.NodePoolResponse'
type: array
uniqueItems: false
org_key:
type: string
org_secret:
type: string
random_token:
type: string
region:
@@ -1037,6 +1043,8 @@ components:
type: object
models.APIKey:
properties:
cluster_id:
type: string
created_at:
format: date-time
type: string
@@ -1046,6 +1054,8 @@ components:
id:
format: uuid
type: string
is_ephemeral:
type: boolean
last_used_at:
format: date-time
type: string
@@ -1056,6 +1066,8 @@ components:
type: string
prefix:
type: string
purpose:
type: string
revoked:
type: boolean
scope:

7
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.5
github.com/aws/aws-sdk-go-v2/credentials v1.19.5
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/dyaksa/archer v1.1.5
github.com/fergusstrange/embedded-postgres v1.33.0
@@ -16,7 +16,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/go-playground/validator/v10 v10.29.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
@@ -31,7 +31,6 @@ require (
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
github.com/swaggo/swag/v2 v2.0.0-rc4
)
require (
@@ -62,7 +61,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect

6
go.sum
View File

@@ -40,6 +40,8 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQ
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2 h1:U3ygWUhCpiSPYSHOrRhb3gOl9T5Y3kB8k5Vjs//57bE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
@@ -81,6 +83,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
@@ -114,6 +118,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=

View File

@@ -129,6 +129,12 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
archer.WithTimeout(60*time.Minute),
)
c.Register(
"org_key_sweeper",
OrgKeySweeperWorker(gdb, jobs),
archer.WithInstances(1),
archer.WithTimeout(5*time.Minute),
)
return jobs, nil
}

View File

@@ -40,7 +40,7 @@ func ClusterBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
var clusters []models.Cluster
if err := db.
Preload("BastionServer.SshKey").
Where("status = ?", clusterStatusPending).
Where("status = ?", clusterStatusProvisioning).
Find(&clusters).Error; err != nil {
log.Error().Err(err).Msg("[cluster_bootstrap] query clusters failed")
return nil, err

View File

@@ -74,8 +74,8 @@ func ClusterSetupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
if err != nil {
failCount++
failedIDs = append(failedIDs, c.ID)
logger.Error().Err(err).Str("output", out).Msg("[cluster_setup] make setup failed")
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make setup: %v", err))
logger.Error().Err(err).Str("output", out).Msg("[cluster_setup] make ping-servers failed")
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make ping-servers: %v", err))
continue
}

View File

@@ -0,0 +1,95 @@
package bg
import (
"context"
"time"
"github.com/dyaksa/archer"
"github.com/dyaksa/archer/job"
"github.com/glueops/autoglue/internal/models"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type OrgKeySweeperArgs struct {
IntervalS int `json:"interval_seconds,omitempty"`
RetentionDays int `json:"retention_days,omitempty"`
}
type OrgKeySweeperResult struct {
Status string `json:"status"`
MarkedRevoked int `json:"marked_revoked"`
DeletedEphemeral int `json:"deleted_ephemeral"`
ElapsedMs int `json:"elapsed_ms"`
}
func OrgKeySweeperWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
return func(ctx context.Context, j job.Job) (any, error) {
args := OrgKeySweeperArgs{
IntervalS: 3600,
RetentionDays: 10,
}
start := time.Now()
_ = j.ParseArguments(&args)
if args.IntervalS <= 0 {
args.IntervalS = 3600
}
if args.RetentionDays <= 0 {
args.RetentionDays = 10
}
now := time.Now()
// 1) Mark expired keys as revoked
res1 := db.Model(&models.APIKey{}).
Where("expires_at IS NOT NULL AND expires_at <= ? AND revoked = false", now).
Updates(map[string]any{
"revoked": true,
"updated_at": now,
})
if res1.Error != nil {
log.Error().Err(res1.Error).Msg("[org_key_sweeper] mark expired revoked failed")
return nil, res1.Error
}
markedRevoked := int(res1.RowsAffected)
// 2) Hard-delete ephemeral keys that are revoked and older than retention
cutoff := now.Add(-time.Duration(args.RetentionDays) * 24 * time.Hour)
res2 := db.
Where("is_ephemeral = ? AND revoked = ? AND updated_at <= ?", true, true, cutoff).
Delete(&models.APIKey{})
if res2.Error != nil {
log.Error().Err(res2.Error).Msg("[org_key_sweeper] delete revoked ephemeral keys failed")
return nil, res2.Error
}
deletedEphemeral := int(res2.RowsAffected)
out := OrgKeySweeperResult{
Status: "ok",
MarkedRevoked: markedRevoked,
DeletedEphemeral: deletedEphemeral,
ElapsedMs: int(time.Since(start).Milliseconds()),
}
log.Info().
Int("marked_revoked", markedRevoked).
Int("deleted_ephemeral", deletedEphemeral).
Msg("[org_key_sweeper] cleanup tick ok")
// Re-enqueue the sweeper
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
_, _ = jobs.Enqueue(
ctx,
uuid.NewString(),
"org_key_sweeper",
args,
archer.WithScheduleTime(next),
archer.WithMaxRetries(1),
)
return out, nil
}
}

View File

@@ -3,6 +3,7 @@ package bg
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
@@ -12,6 +13,7 @@ import (
"github.com/dyaksa/archer"
"github.com/dyaksa/archer/job"
"github.com/glueops/autoglue/internal/auth"
"github.com/glueops/autoglue/internal/mapper"
"github.com/glueops/autoglue/internal/models"
"github.com/glueops/autoglue/internal/utils"
@@ -133,6 +135,52 @@ func ClusterPrepareWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
dtoCluster := mapper.ClusterToDTO(*c)
if c.EncryptedKubeconfig != "" && c.KubeIV != "" && c.KubeTag != "" {
kubeconfig, err := utils.DecryptForOrg(
c.OrganizationID,
c.EncryptedKubeconfig,
c.KubeIV,
c.KubeTag,
db,
)
if err != nil {
fail++
failedIDs = append(failedIDs, c.ID)
failures = append(failures, ClusterPrepareFailure{
ClusterID: c.ID,
Step: "decrypt_kubeconfig",
Reason: err.Error(),
})
clusterLog.Error().Err(err).Msg("[cluster_prepare] decrypt kubeconfig failed")
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
continue
}
dtoCluster.Kubeconfig = &kubeconfig
}
orgKey, orgSecret, err := findOrCreateClusterAutomationKey(
db,
c.OrganizationID,
c.ID,
24*time.Hour,
)
if err != nil {
fail++
failedIDs = append(failedIDs, c.ID)
failures = append(failures, ClusterPrepareFailure{
ClusterID: c.ID,
Step: "create_org_key",
Reason: err.Error(),
})
clusterLog.Error().Err(err).Msg("[cluster_prepare] create org key for payload failed")
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
continue
}
dtoCluster.OrgKey = &orgKey
dtoCluster.OrgSecret = &orgSecret
payloadJSON, err := json.MarshalIndent(dtoCluster, "", " ")
if err != nil {
fail++
@@ -528,3 +576,75 @@ func runMakeOnBastion(
}
return string(out), nil
}
func randomB64URL(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func findOrCreateClusterAutomationKey(
db *gorm.DB,
orgID uuid.UUID,
clusterID uuid.UUID,
ttl time.Duration,
) (orgKey string, orgSecret string, err error) {
now := time.Now()
name := fmt.Sprintf("cluster-%s-bastion", clusterID.String())
// 1) Delete any existing ephemeral cluster-bastion key for this org+cluster
if err := db.Where(
"org_id = ? AND scope = ? AND purpose = ? AND cluster_id = ? AND is_ephemeral = ?",
orgID, "org", "cluster_bastion", clusterID, true,
).Delete(&models.APIKey{}).Error; err != nil {
return "", "", fmt.Errorf("delete existing cluster key: %w", err)
}
// 2) Mint a fresh keypair
keySuffix, err := randomB64URL(16)
if err != nil {
return "", "", fmt.Errorf("entropy_error: %w", err)
}
sec, err := randomB64URL(32)
if err != nil {
return "", "", fmt.Errorf("entropy_error: %w", err)
}
orgKey = "org_" + keySuffix
orgSecret = sec
keyHash := auth.SHA256Hex(orgKey)
secretHash, err := auth.HashSecretArgon2id(orgSecret)
if err != nil {
return "", "", fmt.Errorf("hash_error: %w", err)
}
exp := now.Add(ttl)
prefix := orgKey
if len(prefix) > 12 {
prefix = prefix[:12]
}
rec := models.APIKey{
OrgID: &orgID,
Scope: "org",
Purpose: "cluster_bastion",
ClusterID: &clusterID,
IsEphemeral: true,
Name: name,
KeyHash: keyHash,
SecretHash: &secretHash,
ExpiresAt: &exp,
Revoked: false,
Prefix: &prefix,
}
if err := db.Create(&rec).Error; err != nil {
return "", "", fmt.Errorf("db_error: %w", err)
}
return orgKey, orgSecret, nil
}

View File

@@ -69,7 +69,17 @@ func ListClusters(db *gorm.DB) http.HandlerFunc {
out := make([]dto.ClusterResponse, 0, len(rows))
for _, row := range rows {
out = append(out, clusterToDTO(row))
cr := clusterToDTO(row)
if row.EncryptedKubeconfig != "" && row.KubeIV != "" && row.KubeTag != "" {
kubeconfig, err := utils.DecryptForOrg(orgID, row.EncryptedKubeconfig, row.KubeIV, row.KubeTag, db)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "kubeconfig_decrypt_failed", "failed to decrypt kubeconfig")
return
}
cr.Kubeconfig = &kubeconfig
}
out = append(out, cr)
}
utils.WriteJSON(w, http.StatusOK, out)
}
@@ -131,7 +141,18 @@ func GetCluster(db *gorm.DB) http.HandlerFunc {
return
}
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
resp := clusterToDTO(cluster)
if cluster.EncryptedKubeconfig != "" && cluster.KubeIV != "" && cluster.KubeTag != "" {
kubeconfig, err := utils.DecryptForOrg(orgID, cluster.EncryptedKubeconfig, cluster.KubeIV, cluster.KubeTag, db)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "kubeconfig_decrypt_failed", "failed to decrypt kubeconfig")
return
}
resp.Kubeconfig = &kubeconfig
}
utils.WriteJSON(w, http.StatusOK, resp)
}
}

View File

@@ -24,6 +24,9 @@ type ClusterResponse struct {
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
DockerImage string `json:"docker_image"`
DockerTag string `json:"docker_tag"`
Kubeconfig *string `json:"kubeconfig,omitempty"`
OrgKey *string `json:"org_key,omitempty"`
OrgSecret *string `json:"org_secret,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -828,16 +828,16 @@ func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
}
out := make([]dto.LabelResponse, 0, len(np.Taints))
for _, taint := range np.Taints {
for _, label := range np.Labels {
out = append(out, dto.LabelResponse{
AuditFields: common.AuditFields{
ID: taint.ID,
OrganizationID: taint.OrganizationID,
CreatedAt: taint.CreatedAt,
UpdatedAt: taint.UpdatedAt,
ID: label.ID,
OrganizationID: label.OrganizationID,
CreatedAt: label.CreatedAt,
UpdatedAt: label.UpdatedAt,
},
Key: taint.Key,
Value: taint.Value,
Key: label.Key,
Value: label.Value,
})
}
utils.WriteJSON(w, http.StatusOK, out)

View File

@@ -585,13 +585,22 @@ func CreateOrgKey(db *gorm.DB) http.HandlerFunc {
exp = &e
}
prefix := orgKey
if len(prefix) > 12 {
prefix = prefix[:12]
}
rec := models.APIKey{
OrgID: &oid,
Scope: "org",
Purpose: "user",
IsEphemeral: false,
Name: req.Name,
KeyHash: keyHash,
SecretHash: &secretHash,
ExpiresAt: exp,
Revoked: false,
Prefix: &prefix,
}
if err := db.Create(&rec).Error; err != nil {
utils.WriteError(w, 500, "db_error", err.Error())

View File

@@ -8,12 +8,15 @@ import (
type APIKey struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
Scope string `gorm:"not null;default:''" json:"scope"`
Purpose string `json:"purpose"`
ClusterID *uuid.UUID `json:"cluster_id,omitempty"`
IsEphemeral bool `json:"is_ephemeral"`
Name string `gorm:"not null;default:''" json:"name"`
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
Scope string `gorm:"not null;default:''" json:"scope"`
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
SecretHash *string `json:"-"`
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
ExpiresAt *time.Time `json:"expires_at,omitempty" format:"date-time"`
Revoked bool `gorm:"not null;default:false" json:"revoked"`
Prefix *string `json:"prefix,omitempty"`

View File

@@ -38,7 +38,7 @@
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@tailwindcss/vite": "^4.1.17",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -46,13 +46,13 @@
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.557.0",
"lucide-react": "^0.561.0",
"motion": "^12.23.26",
"next-themes": "^0.4.6",
"rapidoc": "^9.3.8",
"react": "^19.2.1",
"react": "^19.2.3",
"react-day-picker": "^9.12.0",
"react-dom": "^19.2.1",
"react-dom": "^19.2.3",
"react-hook-form": "^7.68.0",
"react-icons": "^5.5.0",
"react-resizable-panels": "^3.0.6",
@@ -60,27 +60,27 @@
"recharts": "2.15.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tailwindcss": "^4.1.18",
"vaul": "^1.1.2",
"zod": "^4.1.13"
},
"devDependencies": {
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.2",
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
"@types/node": "24.10.2",
"@types/node": "25.0.1",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "5.1.2",
"eslint": "9.39.1",
"eslint": "9.39.2",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-react-refresh": "0.4.24",
"globals": "16.5.0",
"prettier": "3.7.4",
"prettier-plugin-tailwindcss": "0.7.2",
"shadcn": "3.5.2",
"shadcn": "3.6.0",
"tw-animate-css": "1.4.0",
"typescript": "5.9.3",
"typescript-eslint": "8.49.0",
"typescript-eslint": "8.50.0",
"vite": "7.2.7"
}
}

View File

@@ -8,6 +8,7 @@ import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { Badge } from "@/components/ui/badge.tsx"
import { Button } from "@/components/ui/button.tsx"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
import {
@@ -35,10 +36,12 @@ import {
TableRow,
} from "@/components/ui/table.tsx"
// 1) No coerce; well do the conversion in onChange
const createSchema = z.object({
name: z.string(),
expires_in_hours: z.number().min(1).max(43800),
expires_in_hours: z.number().int().min(1).max(43800),
})
type CreateValues = z.infer<typeof createSchema>
export const OrgApiKeys = () => {
@@ -52,6 +55,7 @@ export const OrgApiKeys = () => {
queryFn: () => withRefresh(() => api.listOrgKeys({ id: orgId! })),
})
// 2) Form holds numbers directly
const form = useForm<CreateValues>({
resolver: zodResolver(createSchema),
defaultValues: {
@@ -71,7 +75,7 @@ export const OrgApiKeys = () => {
void qc.invalidateQueries({ queryKey: ["org:keys", orgId] })
setShowSecret({ key: resp.org_key, secret: resp.org_secret })
toast.success("Key created")
form.reset({ name: "", expires_in_hours: undefined })
form.reset({ name: "", expires_in_hours: 720 })
},
onError: (e: any) => toast.error(e?.message ?? "Failed to create key"),
})
@@ -124,7 +128,17 @@ export const OrgApiKeys = () => {
<FormItem>
<FormLabel>Expires In (hours)</FormLabel>
<FormControl>
<Input placeholder="e.g. 720" {...field} />
<Input
type="number"
placeholder="e.g. 720"
{...field}
// 3) Convert string → number (or undefined if empty)
value={field.value ?? ""}
onChange={(e) => {
const v = e.target.value
field.onChange(v === "" ? undefined : Number(v))
}}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -148,6 +162,7 @@ export const OrgApiKeys = () => {
<TableHead>Scope</TableHead>
<TableHead>Created</TableHead>
<TableHead>Expires</TableHead>
<TableHead>Status</TableHead>
<TableHead className="w-28" />
</TableRow>
</TableHeader>
@@ -160,6 +175,33 @@ export const OrgApiKeys = () => {
<TableCell>
{k.expires_at ? new Date(k.expires_at).toLocaleString() : "-"}
</TableCell>
<TableCell>
{(() => {
const isExpired = k.expires_at ? new Date(k.expires_at) <= new Date() : false
if (k.revoked) {
return (
<Badge variant="destructive" className="font-mono">
Revoked
</Badge>
)
}
if (isExpired) {
return (
<Badge variant="outline" className="font-mono">
Expired
</Badge>
)
}
return (
<Badge variant="secondary" className="font-mono">
Active
</Badge>
)
})()}
</TableCell>
<TableCell className="text-right">
<Button variant="destructive" size="sm" onClick={() => deleteMut.mutate(k.id!)}>
Delete

File diff suppressed because it is too large Load Diff