Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ephemeral.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:
run: |
cd infra
make ephemeral-init
make ephemeral-apply WORKSPACE=${{ steps.cluster-info.outputs.cluster_name }}
make ephemeral-apply WORKSPACE=${{ steps.cluster-info.outputs.cluster_name }} MINIMAL=true

# ==================== BUILD AND DEPLOY (PR opened/updated) ====================

Expand Down
10 changes: 1 addition & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ FROM alpine:3.19
RUN apk --no-cache add \
ca-certificates \
curl \
kubectl \
openssh \
&& wget -O /usr/local/bin/virtctl \
https://github.com/kubevirt/kubevirt/releases/download/v1.5.1/virtctl-v1.5.1-linux-amd64 \
&& chmod +x /usr/local/bin/virtctl
kubectl

RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
Expand All @@ -41,10 +37,6 @@ RUN chown -R appuser:appgroup /app

USER appuser

RUN mkdir -p /home/appuser/.ssh && \
chown -R appuser:appgroup /home/appuser/.ssh && \
chmod 700 /home/appuser/.ssh

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
Expand Down
1 change: 1 addition & 0 deletions kustomize/base/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ data:
GOLDEN_IMAGE_NAME: new-golden-image-1-33-0
GOLDEN_IMAGE_NAMESPACE: vm-templates
VALIDATE_GOLDEN_IMAGE: true
TERMINAL_MGMT_URL: https://terminal.cks.fullstack.pw
TEMPLATE_PATH: /app/templates
SCENARIOS_PATH: /app/scenarios
1 change: 1 addition & 0 deletions kustomize/ephemeral-base/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ data:
GOLDEN_IMAGE_NAME: new-golden-image-1-33-0
GOLDEN_IMAGE_NAMESPACE: vm-templates
VALIDATE_GOLDEN_IMAGE: true
TERMINAL_MGMT_URL: https://terminal.cks.fullstack.pw
TEMPLATE_PATH: /app/templates
SCENARIOS_PATH: /app/scenarios
11 changes: 2 additions & 9 deletions src/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"github.com/fullstack-pw/cks/backend/internal/scenarios"
"github.com/fullstack-pw/cks/backend/internal/services"
"github.com/fullstack-pw/cks/backend/internal/sessions"
"github.com/fullstack-pw/cks/backend/internal/terminal"
"github.com/fullstack-pw/cks/backend/internal/validation"
)

Expand Down Expand Up @@ -194,13 +193,8 @@ func main() {
logger.WithError(err).Fatal("Failed to create kubevirt client")
}

// Rest of the main function remains the same...
// Create unified validator (ADD THIS)
unifiedValidator := validation.NewUnifiedValidator(kubevirtClient, logger)

// Create terminal manager (existing)
terminalManager := terminal.NewManager(kubeClient, kubevirtClient, k8sConfig, logger)

// Create scenario manager first
scenarioManager, err := scenarios.NewScenarioManager(cfg.ScenariosPath, logger)
if err != nil {
Expand All @@ -221,9 +215,8 @@ func main() {

// Create service layer implementations
sessionService := services.NewSessionService(sessionManager)
terminalService := services.NewTerminalService(terminalManager)
terminalService := services.NewTerminalService(kubevirtClient, cfg)
scenarioService := services.NewScenarioService(scenarioManager)
sessionManager.SetTerminalCleanupFunc(terminalService.CleanupSessionSSH)

// Create and register controllers
sessionController := controllers.NewSessionController(sessionService, scenarioService, logger, unifiedValidator)
Expand All @@ -243,7 +236,7 @@ func main() {
Addr: fmt.Sprintf("%s:%d", cfg.ServerHost, cfg.ServerPort),
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 300 * time.Second, // Longer timeout for WebSockets
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}

Expand Down
5 changes: 1 addition & 4 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ go 1.24.0
toolchain go1.24.1

require (
github.com/creack/pty v1.1.24
github.com/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/prometheus/client_golang v1.19.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.31.8
k8s.io/apimachinery v0.31.8
Expand Down Expand Up @@ -50,6 +47,7 @@ require (
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand All @@ -66,7 +64,6 @@ require (
github.com/openshift/client-go v0.0.0-20210112165513-ebc401615f47 // indirect
github.com/openshift/custom-resource-status v1.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.68.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down
6 changes: 6 additions & 0 deletions src/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type Config struct {
GoldenImageNamespace string // Namespace where golden images are stored
ValidateGoldenImage bool // Whether to validate image exists before VM creation

// Terminal management
TerminalMgmtURL string

// Scenario settings
ScenariosPath string
}
Expand Down Expand Up @@ -77,6 +80,9 @@ func LoadConfig() (*Config, error) {
GoldenImageNamespace: getEnv("GOLDEN_IMAGE_NAMESPACE", "vm-templates"),
ValidateGoldenImage: getEnvAsBool("VALIDATE_GOLDEN_IMAGE", true),

// Terminal management
TerminalMgmtURL: getEnv("TERMINAL_MGMT_URL", "https://terminal.cks.fullstack.pw"),

// Scenario defaults
ScenariosPath: getEnv("SCENARIOS_PATH", "scenarios"),
}
Expand Down
130 changes: 8 additions & 122 deletions src/internal/controllers/terminal_controller.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// backend/internal/controllers/terminal_controller.go

package controllers

import (
Expand All @@ -13,14 +11,12 @@ import (
"github.com/fullstack-pw/cks/backend/internal/services"
)

// TerminalController handles HTTP requests related to terminal sessions
type TerminalController struct {
terminalService services.TerminalService
sessionService services.SessionService
logger *logrus.Logger
}

// NewTerminalController creates a new terminal controller
func NewTerminalController(
terminalService services.TerminalService,
sessionService services.SessionService,
Expand All @@ -33,20 +29,10 @@ func NewTerminalController(
}
}

// RegisterRoutes registers terminal-related routes
func (tc *TerminalController) RegisterRoutes(router *gin.Engine) {
// Terminal routes
router.POST("/api/v1/sessions/:id/terminals", tc.CreateTerminal)

terminals := router.Group("/api/v1/terminals")
{
terminals.GET("/:id/attach", tc.AttachTerminal)
terminals.POST("/:id/resize", tc.ResizeTerminal)
terminals.DELETE("/:id", tc.CloseTerminal)
}
}

// CreateTerminal creates a new terminal session or reuses existing one
func (tc *TerminalController) CreateTerminal(c *gin.Context) {
sessionID := c.Param("id")

Expand All @@ -57,15 +43,13 @@ func (tc *TerminalController) CreateTerminal(c *gin.Context) {
return
}

// Check if session exists
session, err := tc.sessionService.GetSession(sessionID)
if err != nil {
tc.logger.WithError(err).WithField("sessionID", sessionID).Error("Session not found")
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Session not found: %v", err)})
return
}

// Check if session is in running state
if session.Status != models.SessionStatusRunning {
tc.logger.WithFields(logrus.Fields{
"sessionID": sessionID,
Expand All @@ -77,7 +61,6 @@ func (tc *TerminalController) CreateTerminal(c *gin.Context) {
return
}

// Validate target
targetVM := ""
switch request.Target {
case "control-plane":
Expand All @@ -90,117 +73,20 @@ func (tc *TerminalController) CreateTerminal(c *gin.Context) {
return
}

// Always create or get terminal session
terminalID, err := tc.terminalService.CreateSession(sessionID, session.Namespace, targetVM)
terminalURL, err := tc.terminalService.GetTerminalURL(c.Request.Context(), session.Namespace, targetVM)
if err != nil {
tc.logger.WithError(err).Error("Failed to create terminal session")
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create terminal: %v", err)})
tc.logger.WithError(err).Error("Failed to get terminal URL")
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get terminal URL: %v", err)})
return
}

// Store terminal info in session
err = tc.sessionService.StoreTerminalSession(sessionID, terminalID, request.Target)
if err != nil {
tc.logger.WithError(err).Error("Failed to store terminal session info")
// Continue anyway, don't fail the request
}

tc.logger.WithFields(logrus.Fields{
"sessionID": sessionID,
"terminalID": terminalID,
"target": request.Target,
}).Info("Terminal session created/retrieved")
"sessionID": sessionID,
"target": request.Target,
"terminalURL": terminalURL,
}).Info("Terminal URL generated")

c.JSON(http.StatusOK, models.CreateTerminalResponse{
TerminalID: terminalID,
TerminalURL: terminalURL,
})
}

// AttachTerminal handles WebSocket connection to a terminal
func (tc *TerminalController) AttachTerminal(c *gin.Context) {
terminalID := c.Param("id")

tc.logger.WithField("terminalID", terminalID).Info("Attaching to terminal session")

// Add CORS headers for WebSocket connections
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
c.Header("Access-Control-Allow-Credentials", "true")

// Handle WebSocket using the service
tc.terminalService.HandleTerminal(c.Writer, c.Request, terminalID)
}

// ResizeTerminal handles terminal resize events
func (tc *TerminalController) ResizeTerminal(c *gin.Context) {
terminalID := c.Param("id")

var request models.ResizeTerminalRequest
if err := c.ShouldBindJSON(&request); err != nil {
tc.logger.WithError(err).Error("Invalid resize request")
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid resize request"})
return
}

// Validate dimensions
if request.Rows == 0 || request.Cols == 0 {
tc.logger.WithFields(logrus.Fields{
"terminalID": terminalID,
"rows": request.Rows,
"cols": request.Cols,
}).Error("Invalid terminal dimensions")
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid terminal dimensions"})
return
}

// Resize terminal using the service
err := tc.terminalService.ResizeTerminal(terminalID, request.Rows, request.Cols)
if err != nil {
tc.logger.WithError(err).WithField("terminalID", terminalID).Error("Failed to resize terminal")
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to resize terminal: %v", err)})
return
}

tc.logger.WithFields(logrus.Fields{
"terminalID": terminalID,
"rows": request.Rows,
"cols": request.Cols,
}).Debug("Terminal resized")

c.JSON(http.StatusOK, gin.H{"message": "Terminal resized"})
}

// CloseTerminal closes a terminal session
func (tc *TerminalController) CloseTerminal(c *gin.Context) {
terminalID := c.Param("id")

// Close terminal session using the service
err := tc.terminalService.CloseSession(terminalID)
if err != nil {
tc.logger.WithError(err).WithField("terminalID", terminalID).Error("Failed to close terminal")
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to close terminal: %v", err)})
return
}

// Unregister terminal session from all sessions
// This needs to be refactored to be more service-oriented
sessions := tc.sessionService.ListSessions()
for _, session := range sessions {
for id := range session.TerminalSessions {
if id == terminalID {
unregErr := tc.sessionService.UnregisterTerminalSession(session.ID, terminalID)
if unregErr != nil {
tc.logger.WithError(unregErr).WithFields(logrus.Fields{
"sessionID": session.ID,
"terminalID": terminalID,
}).Warn("Failed to unregister terminal session, continuing anyway")
}
break
}
}
}

tc.logger.WithField("terminalID", terminalID).Info("Terminal session closed")
c.JSON(http.StatusOK, gin.H{"message": "Terminal closed"})
}
8 changes: 4 additions & 4 deletions src/internal/kubevirt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ func (c *Client) CreateCluster(ctx context.Context, namespace, controlPlaneName,
return c.createCloudInitSecret(ctx, namespace, workerNodeName, "worker", map[string]string{
"JOIN_COMMAND": joinCommand,
"JOIN": joinCommand,
"CONTROL_PLANE_ENDPOINT": fmt.Sprintf("%s.%s.pod.cluster.local", strings.ReplaceAll(c.getVMIP(ctx, namespace, controlPlaneName), ".", "-"), namespace),
"CONTROL_PLANE_IP": c.getVMIP(ctx, namespace, controlPlaneName),
"CONTROL_PLANE_ENDPOINT": fmt.Sprintf("%s.%s.pod.cluster.local", strings.ReplaceAll(c.GetVMIP(ctx, namespace, controlPlaneName), ".", "-"), namespace),
"CONTROL_PLANE_IP": c.GetVMIP(ctx, namespace, controlPlaneName),
"CONTROL_PLANE_VM_NAME": controlPlaneName,
})
})
Expand Down Expand Up @@ -695,8 +695,8 @@ func (c *Client) getJoinCommand(ctx context.Context, namespace, controlPlaneName
return joinCommand, nil
}

// getVMIP gets the IP address of a VM
func (c *Client) getVMIP(ctx context.Context, namespace, vmName string) string {
// GetVMIP gets the IP address of a VM
func (c *Client) GetVMIP(ctx context.Context, namespace, vmName string) string {
var ip string
err := wait.PollImmediate(5*time.Second, 2*time.Minute, func() (bool, error) {
// Get VM instance
Expand Down
8 changes: 1 addition & 7 deletions src/internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,7 @@ type CreateTerminalRequest struct {

// CreateTerminalResponse represents a response to a create terminal request
type CreateTerminalResponse struct {
TerminalID string `json:"terminalId"`
}

// ResizeTerminalRequest represents a request to resize a terminal
type ResizeTerminalRequest struct {
Rows uint16 `json:"rows"`
Cols uint16 `json:"cols"`
TerminalURL string `json:"terminalUrl"`
}

type SetupCondition struct {
Expand Down
7 changes: 1 addition & 6 deletions src/internal/services/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package services

import (
"context"
"net/http"
"time"

"github.com/fullstack-pw/cks/backend/internal/models"
Expand All @@ -31,11 +30,7 @@ type SessionService interface {

// TerminalService defines the interface for terminal-related operations
type TerminalService interface {
CreateSession(sessionID, namespace, target string) (string, error)
HandleTerminal(w http.ResponseWriter, r *http.Request, terminalID string)
ResizeTerminal(terminalID string, rows, cols uint16) error
CloseSession(terminalID string) error
CleanupSessionSSH(sessionID string) // Add this method
GetTerminalURL(ctx context.Context, namespace, vmName string) (string, error)
}

// ScenarioService defines the interface for scenario-related operations
Expand Down
Loading
Loading