package main import ( "context" "crypto/tls" "crypto/x509" "fmt" "net" "os" "os/signal" "path/filepath" "syscall" "github.com/joho/godotenv" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/health" grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" ttsv1 "gitea.nik4nao.com/nik/home-services/gen/tts/v1" grpcadapter "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/adapters/primary/grpc" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/adapters/secondary/ffmpeg" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/adapters/secondary/inferencesidecar" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/adapters/secondary/jtalk" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/app" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/config" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/logger" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/telemetry" ) // MEMO: auth is not implemented - see ha-gateway/cmd/gateway/main.go's memo // for the same options (shared API key vs mTLS) before exposing this // service to any untrusted network. // version is set at build time via -ldflags "-X main.version=". var version = "dev" func main() { _ = godotenv.Load() cfg, err := config.Load() if err != nil { os.Stderr.WriteString("config error: " + err.Error() + "\n") os.Exit(1) } log := logger.New(cfg.LogFormat, cfg.LogLevel) log.Info("starting tts-gateway", "version", version, "grpc_port", cfg.GRPCPort, "tls_dir", cfg.TLSDir, "otel_endpoint", cfg.OTELEndpoint, "log_level", cfg.LogLevel, "log_format", cfg.LogFormat, "inference_sidecar_addr", cfg.InferenceSidecarAddr, ) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() ctx = logger.WithLogger(ctx, log) shutdown, err := telemetry.Setup(ctx, "tts-gateway", version, cfg) if err != nil { log.Error("telemetry setup failed", "err", err) os.Exit(1) } if cfg.OTELEndpoint != "" { log.Info("telemetry enabled", "endpoint", cfg.OTELEndpoint) } else { log.Debug("telemetry disabled") } normalizer, err := jtalk.NewClient(cfg) if err != nil { log.Error("open_jtalk client setup failed", "err", err) os.Exit(1) } engine := inferencesidecar.NewClient(cfg.InferenceSidecarAddr) encoder := ffmpeg.NewEncoder() ttsApp := app.NewTTSApp(normalizer, engine, encoder) serverOpts := []grpc.ServerOption{ grpc.StatsHandler(otelgrpc.NewServerHandler()), grpc.ChainUnaryInterceptor(grpcadapter.LoggingUnaryInterceptor(log)), } if cfg.TLSDir != "" { creds, err := loadServerCredentials(cfg.TLSDir) if err != nil { log.Error("load mTLS credentials failed", "tls_dir", cfg.TLSDir, "err", err) os.Exit(1) } serverOpts = append(serverOpts, grpc.Creds(creds)) log.Info("mTLS enabled", "tls_dir", cfg.TLSDir) } else { log.Info("mTLS disabled") } srv := grpc.NewServer(serverOpts...) healthSrv := health.NewServer() healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) ttsv1.RegisterTTSServiceServer(srv, grpcadapter.NewTTSGRPC(ttsApp)) grpc_health_v1.RegisterHealthServer(srv, healthSrv) reflection.Register(srv) lis, err := net.Listen("tcp", ":"+cfg.GRPCPort) if err != nil { log.Error("listen failed", "err", err) os.Exit(1) } go func() { log.Info("tts-gateway listening", "addr", lis.Addr().String()) if err := srv.Serve(lis); err != nil { log.Error("serve failed", "err", err) } }() <-ctx.Done() log.Info("shutdown signal received, draining") healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING) srv.GracefulStop() log.Info("shutdown complete") if err := shutdown(context.Background()); err != nil { log.Error("telemetry shutdown error", "err", err) } } func loadServerCredentials(tlsDir string) (credentials.TransportCredentials, error) { cert, err := tls.LoadX509KeyPair( filepath.Join(tlsDir, "tls.crt"), filepath.Join(tlsDir, "tls.key"), ) if err != nil { return nil, fmt.Errorf("load server key pair: %w", err) } caPEM, err := os.ReadFile(filepath.Join(tlsDir, "ca.crt")) if err != nil { return nil, fmt.Errorf("read client CA: %w", err) } clientCAs := x509.NewCertPool() if !clientCAs.AppendCertsFromPEM(caPEM) { return nil, fmt.Errorf("append client CA: invalid PEM") } return credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: clientCAs, ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS13, }), nil }