323 lines
9.1 KiB
Go
323 lines
9.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/cache"
|
|
"github.com/Silo-Server/silo-server/internal/config"
|
|
"github.com/Silo-Server/silo-server/internal/recommendations/embeddings"
|
|
"github.com/Silo-Server/silo-server/internal/s3client"
|
|
)
|
|
|
|
type adminSettingsConnectionCheckRequest struct {
|
|
Values map[string]string `json:"values"`
|
|
DirtyKeys []string `json:"dirty_keys"`
|
|
}
|
|
|
|
type connectionCheckResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type s3SettingsCheckClient interface {
|
|
HeadBucket(ctx context.Context, bucket string) error
|
|
}
|
|
|
|
type redisSettingsCheckClient interface {
|
|
Ping(ctx context.Context) error
|
|
Close() error
|
|
}
|
|
|
|
type embeddingsSettingsCheckClient interface {
|
|
Embed(ctx context.Context, texts []string) ([][]float32, error)
|
|
}
|
|
|
|
type redisSettingsCheckAdapter struct {
|
|
client *redis.Client
|
|
}
|
|
|
|
func (a *redisSettingsCheckAdapter) Ping(ctx context.Context) error {
|
|
return a.client.Ping(ctx).Err()
|
|
}
|
|
|
|
func (a *redisSettingsCheckAdapter) Close() error {
|
|
return a.client.Close()
|
|
}
|
|
|
|
var newAdminS3SettingsCheckClient = func(cfg s3client.BucketConfig) s3SettingsCheckClient {
|
|
return s3client.NewClient(cfg)
|
|
}
|
|
|
|
var newAdminRedisSettingsCheckClient = func(cfg config.RedisConfig) (redisSettingsCheckClient, error) {
|
|
client, err := cache.NewRedisClient(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if client == nil {
|
|
return nil, nil
|
|
}
|
|
return &redisSettingsCheckAdapter{client: client}, nil
|
|
}
|
|
|
|
var newAdminEmbeddingsSettingsCheckClient = func(
|
|
cfg embeddings.ClientConfig,
|
|
) embeddingsSettingsCheckClient {
|
|
return embeddings.NewClient(cfg)
|
|
}
|
|
|
|
func (h *AdminHandler) HandleCheckSettingsConnection(w http.ResponseWriter, r *http.Request) {
|
|
if h.SettingsRepo == nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Settings store not configured")
|
|
return
|
|
}
|
|
|
|
kind := chi.URLParam(r, "kind")
|
|
if strings.TrimSpace(kind) == "" {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Check kind is required")
|
|
return
|
|
}
|
|
|
|
var req adminSettingsConnectionCheckRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
if req.Values == nil {
|
|
req.Values = map[string]string{}
|
|
}
|
|
|
|
effectiveSettings, err := h.effectiveSettingsForConnectionCheck(r.Context(), req)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load settings")
|
|
return
|
|
}
|
|
|
|
cfg, err := config.LoadFromDB(effectiveSettings)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
|
return
|
|
}
|
|
|
|
var response connectionCheckResponse
|
|
switch kind {
|
|
case "s3_public", "s3_operational":
|
|
response = checkS3PublicConnection(r.Context(), cfg)
|
|
case "s3_private":
|
|
response = checkS3PrivateConnection(r.Context(), cfg)
|
|
case "redis":
|
|
response = checkRedisConnection(r.Context(), cfg)
|
|
case "recommendations_embedding":
|
|
response = checkRecommendationsEmbeddingConnection(r.Context(), cfg)
|
|
default:
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Unsupported connection check kind")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
func (h *AdminHandler) effectiveSettingsForConnectionCheck(
|
|
ctx context.Context,
|
|
req adminSettingsConnectionCheckRequest,
|
|
) (map[string]string, error) {
|
|
settings, err := h.SettingsRepo.GetAll(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
merged := make(map[string]string, len(settings)+len(h.BootstrapSensitiveValues)+len(req.DirtyKeys))
|
|
for key, value := range settings {
|
|
merged[key] = value
|
|
}
|
|
for key, value := range h.BootstrapSensitiveValues {
|
|
if value == "" {
|
|
continue
|
|
}
|
|
merged[key] = value
|
|
}
|
|
for _, key := range req.DirtyKeys {
|
|
merged[key] = req.Values[key]
|
|
}
|
|
|
|
return merged, nil
|
|
}
|
|
|
|
func checkS3PublicConnection(ctx context.Context, cfg *config.Config) connectionCheckResponse {
|
|
if strings.TrimSpace(cfg.S3.Public.Endpoint) == "" {
|
|
return connectionCheckResponse{Success: false, Message: "S3 endpoint is required."}
|
|
}
|
|
if strings.TrimSpace(cfg.S3.Public.Bucket) == "" {
|
|
return connectionCheckResponse{Success: false, Message: "S3 bucket is required."}
|
|
}
|
|
|
|
urlAuth := strings.TrimSpace(cfg.S3.Public.URLAuth)
|
|
switch urlAuth {
|
|
case "", s3client.URLAuthPresigned:
|
|
case s3client.URLAuthPublic:
|
|
if strings.TrimSpace(cfg.S3.Public.ReadEndpoint) == "" {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: "A public endpoint is required when URL auth is set to Public.",
|
|
}
|
|
}
|
|
case s3client.URLAuthCloudflareToken:
|
|
if strings.TrimSpace(cfg.S3.Public.ReadEndpoint) == "" {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: "A public endpoint is required when URL auth is set to Cloudflare Token.",
|
|
}
|
|
}
|
|
if strings.TrimSpace(cfg.S3.Public.TokenSecret) == "" {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: "A token secret is required when URL auth is set to Cloudflare Token.",
|
|
}
|
|
}
|
|
default:
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: fmt.Sprintf("Unsupported S3 URL auth method %q.", urlAuth),
|
|
}
|
|
}
|
|
|
|
client := newAdminS3SettingsCheckClient(s3client.BucketConfig{
|
|
Endpoint: cfg.S3.Public.Endpoint,
|
|
PublicEndpoint: cfg.S3.Public.ReadEndpoint,
|
|
Region: cfg.S3.Public.Region,
|
|
Bucket: cfg.S3.Public.Bucket,
|
|
KeyPrefix: cfg.S3.Public.KeyPrefix,
|
|
AccessKey: cfg.S3.Public.AccessKey,
|
|
SecretKey: cfg.S3.Public.SecretKey,
|
|
PathStyle: cfg.S3.Public.PathStyle,
|
|
URLAuth: cfg.S3.Public.URLAuth,
|
|
TokenSecret: cfg.S3.Public.TokenSecret,
|
|
TokenParam: cfg.S3.Public.TokenParam,
|
|
TokenTTL: cfg.S3.Public.TokenTTL,
|
|
})
|
|
|
|
checkCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
if err := client.HeadBucket(checkCtx, cfg.S3.Public.Bucket); err != nil {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: fmt.Sprintf("S3 connection check failed: %v", err),
|
|
}
|
|
}
|
|
|
|
return connectionCheckResponse{
|
|
Success: true,
|
|
Message: "S3 connection successful.",
|
|
}
|
|
}
|
|
|
|
func checkS3PrivateConnection(ctx context.Context, cfg *config.Config) connectionCheckResponse {
|
|
if strings.TrimSpace(cfg.S3.Private.Endpoint) == "" {
|
|
return connectionCheckResponse{Success: false, Message: "S3 endpoint is required."}
|
|
}
|
|
if strings.TrimSpace(cfg.S3.Private.Bucket) == "" {
|
|
return connectionCheckResponse{Success: false, Message: "S3 bucket is required."}
|
|
}
|
|
|
|
client := newAdminS3SettingsCheckClient(s3client.BucketConfig{
|
|
Endpoint: cfg.S3.Private.Endpoint,
|
|
Region: cfg.S3.Private.Region,
|
|
Bucket: cfg.S3.Private.Bucket,
|
|
KeyPrefix: cfg.S3.Private.KeyPrefix,
|
|
AccessKey: cfg.S3.Private.AccessKey,
|
|
SecretKey: cfg.S3.Private.SecretKey,
|
|
PathStyle: cfg.S3.Private.PathStyle,
|
|
})
|
|
|
|
checkCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
if err := client.HeadBucket(checkCtx, cfg.S3.Private.Bucket); err != nil {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: fmt.Sprintf("S3 connection check failed: %v", err),
|
|
}
|
|
}
|
|
|
|
return connectionCheckResponse{
|
|
Success: true,
|
|
Message: "S3 connection successful.",
|
|
}
|
|
}
|
|
|
|
func checkRedisConnection(ctx context.Context, cfg *config.Config) connectionCheckResponse {
|
|
if strings.TrimSpace(cfg.Redis.URL) == "" {
|
|
return connectionCheckResponse{Success: false, Message: "Redis URL is required."}
|
|
}
|
|
|
|
client, err := newAdminRedisSettingsCheckClient(cfg.Redis)
|
|
if err != nil {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: fmt.Sprintf("Redis connection check failed: %v", err),
|
|
}
|
|
}
|
|
if client == nil {
|
|
return connectionCheckResponse{Success: false, Message: "Redis URL is required."}
|
|
}
|
|
defer client.Close()
|
|
|
|
checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
if err := client.Ping(checkCtx); err != nil {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: fmt.Sprintf("Redis connection check failed: %v", err),
|
|
}
|
|
}
|
|
|
|
return connectionCheckResponse{
|
|
Success: true,
|
|
Message: "Redis connection successful.",
|
|
}
|
|
}
|
|
|
|
func checkRecommendationsEmbeddingConnection(
|
|
ctx context.Context,
|
|
cfg *config.Config,
|
|
) connectionCheckResponse {
|
|
if strings.TrimSpace(cfg.Recommendations.EmbeddingBaseURL) == "" {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: "Embedding base URL is required.",
|
|
}
|
|
}
|
|
if strings.TrimSpace(cfg.Recommendations.EmbeddingModel) == "" {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: "Embedding model is required.",
|
|
}
|
|
}
|
|
|
|
client := newAdminEmbeddingsSettingsCheckClient(embeddings.ClientConfig{
|
|
BaseURL: cfg.Recommendations.EmbeddingBaseURL,
|
|
Model: cfg.Recommendations.EmbeddingModel,
|
|
APIKey: cfg.Recommendations.EmbeddingAuthToken,
|
|
})
|
|
|
|
checkCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
|
defer cancel()
|
|
if _, err := client.Embed(checkCtx, []string{"silo connection test"}); err != nil {
|
|
return connectionCheckResponse{
|
|
Success: false,
|
|
Message: fmt.Sprintf("Embedding connection check failed: %v", err),
|
|
}
|
|
}
|
|
|
|
return connectionCheckResponse{
|
|
Success: true,
|
|
Message: "Embedding connection successful.",
|
|
}
|
|
}
|