ai_gateway_coderd_key -> ai_gateway_keys table rename

This commit is contained in:
Paweł Banaszewski
2026-05-29 14:47:39 +00:00
parent 1b734644c8
commit f4042fec96
8 changed files with 126 additions and 126 deletions
@@ -1,22 +0,0 @@
package aigatewaycoderdkey_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/aigatewaycoderdkey"
"github.com/coder/coder/v2/coderd/apikey"
)
func TestNew(t *testing.T) {
t.Parallel()
params, key, err := aigatewaycoderdkey.New("test-key")
require.NoError(t, err)
require.Len(t, key, aigatewaycoderdkey.KeyLength)
require.Len(t, params.SecretPrefix, aigatewaycoderdkey.KeyPrefixLength)
require.Equal(t, key[:aigatewaycoderdkey.KeyPrefixLength], params.SecretPrefix)
require.True(t, apikey.ValidateHash(params.HashedSecret, key))
require.False(t, apikey.ValidateHash(params.HashedSecret, key[aigatewaycoderdkey.KeyPrefixLength:]))
}
@@ -1,4 +1,4 @@
package aigatewaycoderdkey package aigatewaykey
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
@@ -21,14 +21,14 @@ const (
// New generates an AI Gateway Coderd key. Returns InsertParams ready // New generates an AI Gateway Coderd key. Returns InsertParams ready
// for the database query. // for the database query.
func New(name string) (database.InsertAIGatewayCoderdKeyParams, string, error) { func New(name string) (database.InsertAIGatewayKeyParams, string, error) {
secret, hashed, err := apikey.GenerateSecret(KeyLength) secret, hashed, err := apikey.GenerateSecret(KeyLength)
if err != nil { if err != nil {
return database.InsertAIGatewayCoderdKeyParams{}, "", xerrors.Errorf("generate secret: %w", err) return database.InsertAIGatewayKeyParams{}, "", xerrors.Errorf("generate secret: %w", err)
} }
visiblePrefix := secret[:KeyPrefixLength] visiblePrefix := secret[:KeyPrefixLength]
return database.InsertAIGatewayCoderdKeyParams{ return database.InsertAIGatewayKeyParams{
ID: uuid.New(), ID: uuid.New(),
Name: name, Name: name,
SecretPrefix: visiblePrefix, SecretPrefix: visiblePrefix,
+22
View File
@@ -0,0 +1,22 @@
package aigatewaykey_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/aigatewaykey"
"github.com/coder/coder/v2/coderd/apikey"
)
func TestNew(t *testing.T) {
t.Parallel()
params, key, err := aigatewaykey.New("test-key")
require.NoError(t, err)
require.Len(t, key, aigatewaykey.KeyLength)
require.Len(t, params.SecretPrefix, aigatewaykey.KeyPrefixLength)
require.Equal(t, key[:aigatewaykey.KeyPrefixLength], params.SecretPrefix)
require.True(t, apikey.ValidateHash(params.HashedSecret, key))
require.False(t, apikey.ValidateHash(params.HashedSecret, key[aigatewaykey.KeyPrefixLength:]))
}
@@ -11,9 +11,9 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
// AIGatewayCoderdKey is a shared secret used by a standalone AI Gateway // AIGatewayKey is a shared secret used by a standalone AI Gateway
// to authenticate into coderd. // to authenticate into coderd.
type AIGatewayCoderdKey struct { type AIGatewayKey struct {
ID uuid.UUID `json:"id" format:"uuid"` ID uuid.UUID `json:"id" format:"uuid"`
Name string `json:"name"` Name string `json:"name"`
KeyPrefix string `json:"key_prefix"` KeyPrefix string `json:"key_prefix"`
@@ -21,14 +21,14 @@ type AIGatewayCoderdKey struct {
LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"` LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"`
} }
// CreateAIGatewayCoderdKeyRequest requests a new AI Gateway coderd key. // CreateAIGatewayKeyRequest requests a new AI Gateway key.
type CreateAIGatewayCoderdKeyRequest struct { type CreateAIGatewayKeyRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
} }
// CreateAIGatewayCoderdKeyResponse returns all key information. // CreateAIGatewayKeyResponse returns all key information.
// Key value is only returned here and cannot be recovered afterwards. // Key value is only returned here and cannot be recovered afterwards.
type CreateAIGatewayCoderdKeyResponse struct { type CreateAIGatewayKeyResponse struct {
ID uuid.UUID `json:"id" format:"uuid"` ID uuid.UUID `json:"id" format:"uuid"`
Name string `json:"name"` Name string `json:"name"`
Key string `json:"key"` Key string `json:"key"`
@@ -36,24 +36,24 @@ type CreateAIGatewayCoderdKeyResponse struct {
CreatedAt time.Time `json:"created_at" format:"date-time"` CreatedAt time.Time `json:"created_at" format:"date-time"`
} }
// CreateAIGatewayCoderdKey creates a new AI Gateway coderd key. // CreateAIGatewayKey creates a new AI Gateway key.
func (c *Client) CreateAIGatewayCoderdKey(ctx context.Context, req CreateAIGatewayCoderdKeyRequest) (CreateAIGatewayCoderdKeyResponse, error) { func (c *Client) CreateAIGatewayKey(ctx context.Context, req CreateAIGatewayKeyRequest) (CreateAIGatewayKeyResponse, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/aibridge/coderd-keys", req) res, err := c.Request(ctx, http.MethodPost, "/api/v2/aibridge/keys", req)
if err != nil { if err != nil {
return CreateAIGatewayCoderdKeyResponse{}, xerrors.Errorf("make request: %w", err) return CreateAIGatewayKeyResponse{}, xerrors.Errorf("make request: %w", err)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != http.StatusCreated { if res.StatusCode != http.StatusCreated {
return CreateAIGatewayCoderdKeyResponse{}, ReadBodyAsError(res) return CreateAIGatewayKeyResponse{}, ReadBodyAsError(res)
} }
var resp CreateAIGatewayCoderdKeyResponse var resp CreateAIGatewayKeyResponse
return resp, json.NewDecoder(res.Body).Decode(&resp) return resp, json.NewDecoder(res.Body).Decode(&resp)
} }
// ListAIGatewayCoderdKeys lists all AI Gateway coderd keys. // ListAIGatewayKeys lists all AI Gateway keys.
func (c *Client) ListAIGatewayCoderdKeys(ctx context.Context) ([]AIGatewayCoderdKey, error) { func (c *Client) ListAIGatewayKeys(ctx context.Context) ([]AIGatewayKey, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/aibridge/coderd-keys", nil) res, err := c.Request(ctx, http.MethodGet, "/api/v2/aibridge/keys", nil)
if err != nil { if err != nil {
return nil, xerrors.Errorf("make request: %w", err) return nil, xerrors.Errorf("make request: %w", err)
} }
@@ -62,14 +62,14 @@ func (c *Client) ListAIGatewayCoderdKeys(ctx context.Context) ([]AIGatewayCoderd
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res) return nil, ReadBodyAsError(res)
} }
var resp []AIGatewayCoderdKey var resp []AIGatewayKey
return resp, json.NewDecoder(res.Body).Decode(&resp) return resp, json.NewDecoder(res.Body).Decode(&resp)
} }
// DeleteAIGatewayCoderdKey deletes an AI Gateway coderd key by ID. // DeleteAIGatewayKey deletes an AI Gateway key by ID.
func (c *Client) DeleteAIGatewayCoderdKey(ctx context.Context, id uuid.UUID) error { func (c *Client) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, res, err := c.Request(ctx, http.MethodDelete,
fmt.Sprintf("/api/v2/aibridge/coderd-keys/%s", id.String()), nil) fmt.Sprintf("/api/v2/aibridge/keys/%s", id.String()), nil)
if err != nil { if err != nil {
return xerrors.Errorf("make request: %w", err) return xerrors.Errorf("make request: %w", err)
} }
@@ -10,7 +10,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/coder/coder/v2/coderd/aigatewaycoderdkey" "github.com/coder/coder/v2/coderd/aigatewaykey"
"github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpapi"
@@ -24,20 +24,20 @@ const maxKeyInsertAttempts = 7
// nameFormatDetail is the human-readable description of valid key names. // nameFormatDetail is the human-readable description of valid key names.
const nameFormatDetail = "Must be 64 characters or fewer, lowercase letters, numbers, and non-consecutive hyphens, cannot start or end with a hyphen." const nameFormatDetail = "Must be 64 characters or fewer, lowercase letters, numbers, and non-consecutive hyphens, cannot start or end with a hyphen."
// @Summary Create AI Gateway coderd key // @Summary Create AI Gateway key
// @ID create-ai-gateway-coderd-key // @ID create-ai-gateway-key
// @Security CoderSessionToken // @Security CoderSessionToken
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Tags Enterprise // @Tags Enterprise
// @Param request body codersdk.CreateAIGatewayCoderdKeyRequest true "Create AI Gateway coderd key request" // @Param request body codersdk.CreateAIGatewayKeyRequest true "Create AI Gateway key request"
// @Success 201 {object} codersdk.CreateAIGatewayCoderdKeyResponse // @Success 201 {object} codersdk.CreateAIGatewayKeyResponse
// @Router /api/v2/aibridge/coderd-keys [post] // @Router /api/v2/aibridge/keys [post]
func (api *API) postAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request) { func (api *API) postAIGatewayKey(rw http.ResponseWriter, r *http.Request) {
var ( var (
ctx = r.Context() ctx = r.Context()
auditor = api.AGPL.Auditor.Load() auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.AiGatewayCoderdKey](rw, &audit.RequestParams{ aReq, commitAudit = audit.InitRequest[database.AIGatewayKey](rw, &audit.RequestParams{
Audit: *auditor, Audit: *auditor,
Log: api.Logger, Log: api.Logger,
Request: r, Request: r,
@@ -46,7 +46,7 @@ func (api *API) postAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request)
) )
defer commitAudit() defer commitAudit()
var req codersdk.CreateAIGatewayCoderdKeyRequest var req codersdk.CreateAIGatewayKeyRequest
if !httpapi.Read(ctx, rw, r, &req) { if !httpapi.Read(ctx, rw, r, &req) {
return return
} }
@@ -60,14 +60,14 @@ func (api *API) postAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request)
return return
} }
aReq.New = database.AiGatewayCoderdKey{ aReq.New = database.AIGatewayKey{
ID: row.ID, ID: row.ID,
Name: row.Name, Name: row.Name,
SecretPrefix: row.SecretPrefix, SecretPrefix: row.SecretPrefix,
CreatedAt: row.CreatedAt, CreatedAt: row.CreatedAt,
} }
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateAIGatewayCoderdKeyResponse{ httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateAIGatewayKeyResponse{
ID: row.ID, ID: row.ID,
Name: row.Name, Name: row.Name,
KeyPrefix: row.SecretPrefix, KeyPrefix: row.SecretPrefix,
@@ -77,14 +77,14 @@ func (api *API) postAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request)
} }
// generateAndInsertKey creates fresh key material and attempts an insert. // generateAndInsertKey creates fresh key material and attempts an insert.
func (api *API) generateAndInsertKey(ctx context.Context, name string) (database.InsertAIGatewayCoderdKeyRow, string, error) { func (api *API) generateAndInsertKey(ctx context.Context, name string) (database.InsertAIGatewayKeyRow, string, error) {
params, key, err := aigatewaycoderdkey.New(name) params, key, err := aigatewaykey.New(name)
if err != nil { if err != nil {
return database.InsertAIGatewayCoderdKeyRow{}, "", err return database.InsertAIGatewayKeyRow{}, "", err
} }
row, err := api.Database.InsertAIGatewayCoderdKey(ctx, params) row, err := api.Database.InsertAIGatewayKey(ctx, params)
if err != nil { if err != nil {
return database.InsertAIGatewayCoderdKeyRow{}, "", err return database.InsertAIGatewayKeyRow{}, "", err
} }
return row, key, nil return row, key, nil
} }
@@ -92,8 +92,8 @@ func (api *API) generateAndInsertKey(ctx context.Context, name string) (database
// isRetryableKeyInsertErr returns true for generated-secret collisions. // isRetryableKeyInsertErr returns true for generated-secret collisions.
func isRetryableKeyInsertErr(err error) bool { func isRetryableKeyInsertErr(err error) bool {
return database.IsUniqueViolation(err, return database.IsUniqueViolation(err,
database.UniqueAiGatewayCoderdKeysSecretPrefixIndex, database.UniqueAiGatewayKeysSecretPrefixIndex,
database.UniqueAiGatewayCoderdKeysHashedSecretIndex, database.UniqueAiGatewayKeysHashedSecretIndex,
) )
} }
@@ -102,14 +102,14 @@ func writeKeyInsertError(ctx context.Context, rw http.ResponseWriter, err error)
switch { switch {
case httpapi.IsUnauthorizedError(err): case httpapi.IsUnauthorizedError(err):
httpapi.Forbidden(rw) httpapi.Forbidden(rw)
case database.IsCheckViolation(err, database.CheckAiGatewayCoderdKeysNameCheck): case database.IsCheckViolation(err, database.CheckAiGatewayKeysNameCheck):
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid key name.", Message: "Invalid key name.",
Validations: []codersdk.ValidationError{ Validations: []codersdk.ValidationError{
{Field: "name", Detail: nameFormatDetail}, {Field: "name", Detail: nameFormatDetail},
}, },
}) })
case database.IsUniqueViolation(err, database.UniqueAiGatewayCoderdKeysNameIndex): case database.IsUniqueViolation(err, database.UniqueAiGatewayKeysNameIndex):
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Key name must be unique.", Message: "Key name must be unique.",
Validations: []codersdk.ValidationError{ Validations: []codersdk.ValidationError{
@@ -121,17 +121,17 @@ func writeKeyInsertError(ctx context.Context, rw http.ResponseWriter, err error)
} }
} }
// @Summary List AI Gateway coderd keys // @Summary List AI Gateway keys
// @ID list-ai-gateway-coderd-keys // @ID list-ai-gatewaykeys
// @Security CoderSessionToken // @Security CoderSessionToken
// @Produce json // @Produce json
// @Tags Enterprise // @Tags Enterprise
// @Success 200 {array} codersdk.AIGatewayCoderdKey // @Success 200 {array} codersdk.AIGatewayKey
// @Router /api/v2/aibridge/coderd-keys [get] // @Router /api/v2/aibridge/keys [get]
func (api *API) aiGatewayCoderdKeys(rw http.ResponseWriter, r *http.Request) { func (api *API) aiGatewayKeys(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
rows, err := api.Database.ListAIGatewayCoderdKeys(ctx) rows, err := api.Database.ListAIGatewayKeys(ctx)
if httpapi.IsUnauthorizedError(err) { if httpapi.IsUnauthorizedError(err) {
httpapi.Forbidden(rw) httpapi.Forbidden(rw)
return return
@@ -141,26 +141,26 @@ func (api *API) aiGatewayCoderdKeys(rw http.ResponseWriter, r *http.Request) {
return return
} }
out := make([]codersdk.AIGatewayCoderdKey, 0, len(rows)) out := make([]codersdk.AIGatewayKey, 0, len(rows))
for _, row := range rows { for _, row := range rows {
out = append(out, convertAIGatewayCoderdKey(row)) out = append(out, convertAIGatewayKey(row))
} }
httpapi.Write(ctx, rw, http.StatusOK, out) httpapi.Write(ctx, rw, http.StatusOK, out)
} }
// @Summary Delete AI Gateway coderd key // @Summary Delete AI Gateway key
// @ID delete-ai-gateway-coderd-key // @ID delete-ai-gateway-key
// @Security CoderSessionToken // @Security CoderSessionToken
// @Tags Enterprise // @Tags Enterprise
// @Param key path string true "Key ID" format(uuid) // @Param key path string true "Key ID" format(uuid)
// @Success 204 // @Success 204
// @Router /api/v2/aibridge/coderd-keys/{key} [delete] // @Router /api/v2/aibridge/keys/{key} [delete]
func (api *API) deleteAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request) { func (api *API) deleteAIGatewayKey(rw http.ResponseWriter, r *http.Request) {
var ( var (
ctx = r.Context() ctx = r.Context()
auditor = api.AGPL.Auditor.Load() auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.AiGatewayCoderdKey](rw, &audit.RequestParams{ aReq, commitAudit = audit.InitRequest[database.AIGatewayKey](rw, &audit.RequestParams{
Audit: *auditor, Audit: *auditor,
Log: api.Logger, Log: api.Logger,
Request: r, Request: r,
@@ -178,7 +178,7 @@ func (api *API) deleteAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request
return return
} }
deleted, err := api.Database.DeleteAIGatewayCoderdKey(ctx, id) deleted, err := api.Database.DeleteAIGatewayKey(ctx, id)
if err != nil { if err != nil {
if httpapi.IsUnauthorizedError(err) { if httpapi.IsUnauthorizedError(err) {
httpapi.Forbidden(rw) httpapi.Forbidden(rw)
@@ -192,7 +192,7 @@ func (api *API) deleteAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request
return return
} }
aReq.Old = database.AiGatewayCoderdKey{ aReq.Old = database.AIGatewayKey{
ID: deleted.ID, ID: deleted.ID,
Name: deleted.Name, Name: deleted.Name,
SecretPrefix: deleted.SecretPrefix, SecretPrefix: deleted.SecretPrefix,
@@ -203,13 +203,13 @@ func (api *API) deleteAIGatewayCoderdKey(rw http.ResponseWriter, r *http.Request
rw.WriteHeader(http.StatusNoContent) rw.WriteHeader(http.StatusNoContent)
} }
func convertAIGatewayCoderdKey(row database.ListAIGatewayCoderdKeysRow) codersdk.AIGatewayCoderdKey { func convertAIGatewayKey(row database.ListAIGatewayKeysRow) codersdk.AIGatewayKey {
var lastUsed *time.Time var lastUsed *time.Time
if row.LastUsedAt.Valid { if row.LastUsedAt.Valid {
t := row.LastUsedAt.Time t := row.LastUsedAt.Time
lastUsed = &t lastUsed = &t
} }
return codersdk.AIGatewayCoderdKey{ return codersdk.AIGatewayKey{
ID: row.ID, ID: row.ID,
Name: row.Name, Name: row.Name,
KeyPrefix: row.SecretPrefix, KeyPrefix: row.SecretPrefix,
@@ -12,7 +12,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/aigatewaycoderdkey" "github.com/coder/coder/v2/coderd/aigatewaykey"
"github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
@@ -26,7 +26,7 @@ import (
"github.com/coder/coder/v2/testutil" "github.com/coder/coder/v2/testutil"
) )
func TestAIGatewayCoderdKeys(t *testing.T) { func TestAIGatewayKeys(t *testing.T) {
t.Parallel() t.Parallel()
// Single instance shared by all subtests (except FeatureGate). // Single instance shared by all subtests (except FeatureGate).
@@ -36,22 +36,22 @@ func TestAIGatewayCoderdKeys(t *testing.T) {
//nolint:paralleltest // Subtests share a single coderdenttest instance. //nolint:paralleltest // Subtests share a single coderdenttest instance.
t.Run("CRUD", func(t *testing.T) { t.Run("CRUD", func(t *testing.T) {
keys, err := ownerClient.ListAIGatewayCoderdKeys(ctx) keys, err := ownerClient.ListAIGatewayKeys(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, keys) require.Empty(t, keys)
name := uniqueName(t, "happy") name := uniqueName(t, "happy")
created, err := ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: name}) created, err := ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: name})
require.NoError(t, err) require.NoError(t, err)
require.NotEqual(t, uuid.Nil, created.ID) require.NotEqual(t, uuid.Nil, created.ID)
require.Equal(t, name, created.Name) require.Equal(t, name, created.Name)
require.Len(t, created.KeyPrefix, aigatewaycoderdkey.KeyPrefixLength) require.Len(t, created.KeyPrefix, aigatewaykey.KeyPrefixLength)
require.Len(t, created.Key, aigatewaycoderdkey.KeyLength) require.Len(t, created.Key, aigatewaykey.KeyLength)
require.True(t, strings.HasPrefix(created.Key, created.KeyPrefix), "key must begin with key_prefix") require.True(t, strings.HasPrefix(created.Key, created.KeyPrefix), "key must begin with key_prefix")
require.WithinDuration(t, time.Now(), created.CreatedAt, time.Minute) require.WithinDuration(t, time.Now(), created.CreatedAt, time.Minute)
keys, err = ownerClient.ListAIGatewayCoderdKeys(ctx) keys, err = ownerClient.ListAIGatewayKeys(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, keys, 1) require.Len(t, keys, 1)
require.Equal(t, created.ID, keys[0].ID) require.Equal(t, created.ID, keys[0].ID)
@@ -59,25 +59,25 @@ func TestAIGatewayCoderdKeys(t *testing.T) {
require.Equal(t, created.KeyPrefix, keys[0].KeyPrefix) require.Equal(t, created.KeyPrefix, keys[0].KeyPrefix)
require.Nil(t, keys[0].LastUsedAt) require.Nil(t, keys[0].LastUsedAt)
require.NoError(t, ownerClient.DeleteAIGatewayCoderdKey(ctx, created.ID)) require.NoError(t, ownerClient.DeleteAIGatewayKey(ctx, created.ID))
keys, err = ownerClient.ListAIGatewayCoderdKeys(ctx) keys, err = ownerClient.ListAIGatewayKeys(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, keys) require.Empty(t, keys)
}) })
//nolint:paralleltest // Subtests share a single coderdenttest instance. //nolint:paralleltest // Subtests share a single coderdenttest instance.
t.Run("ListResponseDoesNotLeakSecrets", func(t *testing.T) { t.Run("ListResponseDoesNotLeakSecrets", func(t *testing.T) {
created, err := ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{ created, err := ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{
Name: uniqueName(t, "leak"), Name: uniqueName(t, "leak"),
}) })
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
_ = ownerClient.DeleteAIGatewayCoderdKey(ctx, created.ID) _ = ownerClient.DeleteAIGatewayKey(ctx, created.ID)
}) })
// Raw HTTP read of LIST to confirm the JSON shape. // Raw HTTP read of LIST to confirm the JSON shape.
resp, err := ownerClient.Request(ctx, http.MethodGet, "/api/v2/aibridge/coderd-keys", nil) resp, err := ownerClient.Request(ctx, http.MethodGet, "/api/v2/aibridge/keys", nil)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { _ = resp.Body.Close() }) t.Cleanup(func() { _ = resp.Body.Close() })
require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, http.StatusOK, resp.StatusCode)
@@ -94,46 +94,46 @@ func TestAIGatewayCoderdKeys(t *testing.T) {
//nolint:paralleltest // Subtests share a single coderdenttest instance. //nolint:paralleltest // Subtests share a single coderdenttest instance.
t.Run("CreateValidation", func(t *testing.T) { t.Run("CreateValidation", func(t *testing.T) {
// Empty name -> 400 (validate:"required" on request struct). // Empty name -> 400 (validate:"required" on request struct).
_, err := ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: ""}) _, err := ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: ""})
require.ErrorContains(t, err, "Validation failed") require.ErrorContains(t, err, "Validation failed")
// >64 char name -> 400 (DB check constraint). // >64 char name -> 400 (DB check constraint).
longName := strings.Repeat("a", 65) longName := strings.Repeat("a", 65)
_, err = ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: longName}) _, err = ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: longName})
require.ErrorContains(t, err, "Invalid key name") require.ErrorContains(t, err, "Invalid key name")
// Uppercase name -> 400 (DB check constraint rejects non-lowercase). // Uppercase name -> 400 (DB check constraint rejects non-lowercase).
_, err = ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: "UPPER-CASE"}) _, err = ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: "UPPER-CASE"})
require.ErrorContains(t, err, "Invalid key name") require.ErrorContains(t, err, "Invalid key name")
// Duplicate name -> 400. // Duplicate name -> 400.
name := uniqueName(t, "dup") name := uniqueName(t, "dup")
created, err := ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: name}) created, err := ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: name})
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
_ = ownerClient.DeleteAIGatewayCoderdKey(ctx, created.ID) _ = ownerClient.DeleteAIGatewayKey(ctx, created.ID)
}) })
_, err = ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: name}) _, err = ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: name})
require.ErrorContains(t, err, "must be unique") require.ErrorContains(t, err, "must be unique")
}) })
//nolint:paralleltest // Subtests share a single coderdenttest instance. //nolint:paralleltest // Subtests share a single coderdenttest instance.
t.Run("DeleteValidation", func(t *testing.T) { t.Run("DeleteValidation", func(t *testing.T) {
// Invalid UUID -> 400 (raw request; SDK method accepts uuid.UUID). // Invalid UUID -> 400 (raw request; SDK method accepts uuid.UUID).
resp, err := ownerClient.Request(ctx, http.MethodDelete, "/api/v2/aibridge/coderd-keys/not-a-uuid", nil) resp, err := ownerClient.Request(ctx, http.MethodDelete, "/api/v2/aibridge/keys/not-a-uuid", nil)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { _ = resp.Body.Close() }) t.Cleanup(func() { _ = resp.Body.Close() })
require.Equal(t, http.StatusBadRequest, resp.StatusCode) require.Equal(t, http.StatusBadRequest, resp.StatusCode)
// Delete existing key -> 204 (SDK returns nil error on 204). // Delete existing key -> 204 (SDK returns nil error on 204).
created, err := ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{ created, err := ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{
Name: uniqueName(t, "del"), Name: uniqueName(t, "del"),
}) })
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, ownerClient.DeleteAIGatewayCoderdKey(ctx, created.ID)) require.NoError(t, ownerClient.DeleteAIGatewayKey(ctx, created.ID))
// Unknown UUID -> 404. // Unknown UUID -> 404.
err = ownerClient.DeleteAIGatewayCoderdKey(ctx, uuid.New()) err = ownerClient.DeleteAIGatewayKey(ctx, uuid.New())
var sdkErr *codersdk.Error var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr) require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode()) require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
@@ -143,18 +143,18 @@ func TestAIGatewayCoderdKeys(t *testing.T) {
t.Run("ReturnsForbiddenForNonOwners", func(t *testing.T) { t.Run("ReturnsForbiddenForNonOwners", func(t *testing.T) {
member, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) member, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
_, err := member.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{ _, err := member.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{
Name: uniqueName(t, "denied"), Name: uniqueName(t, "denied"),
}) })
var sdkErr *codersdk.Error var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr) require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
_, err = member.ListAIGatewayCoderdKeys(ctx) _, err = member.ListAIGatewayKeys(ctx)
require.ErrorAs(t, err, &sdkErr) require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
err = member.DeleteAIGatewayCoderdKey(ctx, uuid.New()) err = member.DeleteAIGatewayKey(ctx, uuid.New())
require.ErrorAs(t, err, &sdkErr) require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
}) })
@@ -171,14 +171,14 @@ func TestAIGatewayCoderdKeys(t *testing.T) {
}) })
//nolint:gocritic // Managing AI Gateway coderd keys is owner-only. //nolint:gocritic // Managing AI Gateway coderd keys is owner-only.
_, err := ownerClient.ListAIGatewayCoderdKeys(ctx) _, err := ownerClient.ListAIGatewayKeys(ctx)
var sdkErr *codersdk.Error var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr) require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
}) })
} }
func TestAIGatewayCoderdKeyAudit(t *testing.T) { func TestAIGatewayKeyAudit(t *testing.T) {
t.Parallel() t.Parallel()
db, ps := dbtestutil.NewDB(t) db, ps := dbtestutil.NewDB(t)
@@ -200,15 +200,15 @@ func TestAIGatewayCoderdKeyAudit(t *testing.T) {
name := uniqueName(t, "audit") name := uniqueName(t, "audit")
//nolint:gocritic // Managing AI Gateway coderd keys is owner-only. //nolint:gocritic // Managing AI Gateway coderd keys is owner-only.
created, err := ownerClient.CreateAIGatewayCoderdKey(ctx, codersdk.CreateAIGatewayCoderdKeyRequest{Name: name}) created, err := ownerClient.CreateAIGatewayKey(ctx, codersdk.CreateAIGatewayKeyRequest{Name: name})
require.NoError(t, err) require.NoError(t, err)
//nolint:gocritic // Managing AI Gateway coderd keys is owner-only. //nolint:gocritic // Managing AI Gateway coderd keys is owner-only.
require.NoError(t, ownerClient.DeleteAIGatewayCoderdKey(ctx, created.ID)) require.NoError(t, ownerClient.DeleteAIGatewayKey(ctx, created.ID))
rows, err := db.GetAuditLogsOffset( rows, err := db.GetAuditLogsOffset(
dbauthz.AsSystemRestricted(ctx), dbauthz.AsSystemRestricted(ctx),
database.GetAuditLogsOffsetParams{ database.GetAuditLogsOffsetParams{
ResourceType: string(database.ResourceTypeAiGatewayCoderdKey), ResourceType: string(database.ResourceTypeAIGatewayKey),
LimitOpt: 10, LimitOpt: 10,
}, },
) )
@@ -233,7 +233,7 @@ func TestAIGatewayCoderdKeyAudit(t *testing.T) {
require.Equal(t, http.StatusNoContent, int(deleteLog.StatusCode)) require.Equal(t, http.StatusNoContent, int(deleteLog.StatusCode))
for _, log := range []database.AuditLog{createLog, deleteLog} { for _, log := range []database.AuditLog{createLog, deleteLog} {
require.Equal(t, database.ResourceTypeAiGatewayCoderdKey, log.ResourceType) require.Equal(t, database.ResourceTypeAIGatewayKey, log.ResourceType)
require.Equal(t, created.ID, log.ResourceID) require.Equal(t, created.ID, log.ResourceID)
require.Equal(t, name, log.ResourceTarget) require.Equal(t, name, log.ResourceTarget)
} }
+4 -4
View File
@@ -299,14 +299,14 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
}) })
api.AGPL.APIHandler.Group(func(r chi.Router) { api.AGPL.APIHandler.Group(func(r chi.Router) {
r.Route("/aibridge/coderd-keys", func(r chi.Router) { r.Route("/aibridge/keys", func(r chi.Router) {
r.Use( r.Use(
apiKeyMiddleware, apiKeyMiddleware,
api.RequireFeatureMW(codersdk.FeatureAIBridge), api.RequireFeatureMW(codersdk.FeatureAIBridge),
) )
r.Get("/", api.aiGatewayCoderdKeys) r.Get("/", api.aiGatewayKeys)
r.Post("/", api.postAIGatewayCoderdKey) r.Post("/", api.postAIGatewayKey)
r.Delete("/{key}", api.deleteAIGatewayCoderdKey) r.Delete("/{key}", api.deleteAIGatewayKey)
}) })
}) })
+9 -9
View File
@@ -304,12 +304,12 @@ export interface AIConfig {
readonly chat?: ChatConfig; readonly chat?: ChatConfig;
} }
// From codersdk/aigatewaycoderdkeys.go // From codersdk/aigatewaykeys.go
/** /**
* AIGatewayCoderdKey is a shared secret used by a standalone AI Gateway * AIGatewayKey is a shared secret used by a standalone AI Gateway
* to authenticate into coderd. * to authenticate into coderd.
*/ */
export interface AIGatewayCoderdKey { export interface AIGatewayKey {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly key_prefix: string; readonly key_prefix: string;
@@ -3257,20 +3257,20 @@ export interface ConvertLoginRequest {
readonly password: string; readonly password: string;
} }
// From codersdk/aigatewaycoderdkeys.go // From codersdk/aigatewaykeys.go
/** /**
* CreateAIGatewayCoderdKeyRequest requests a new AI Gateway coderd key. * CreateAIGatewayKeyRequest requests a new AI Gateway key.
*/ */
export interface CreateAIGatewayCoderdKeyRequest { export interface CreateAIGatewayKeyRequest {
readonly name: string; readonly name: string;
} }
// From codersdk/aigatewaycoderdkeys.go // From codersdk/aigatewaykeys.go
/** /**
* CreateAIGatewayCoderdKeyResponse returns all key information. * CreateAIGatewayKeyResponse returns all key information.
* Key value is only returned here and cannot be recovered afterwards. * Key value is only returned here and cannot be recovered afterwards.
*/ */
export interface CreateAIGatewayCoderdKeyResponse { export interface CreateAIGatewayKeyResponse {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly key: string; readonly key: string;