diff --git a/server/internal/api/handlers/cluster.go b/server/internal/api/handlers/cluster.go new file mode 100644 index 0000000..fc56ddb --- /dev/null +++ b/server/internal/api/handlers/cluster.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type ClusterInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Health bool `json:"health"` + ServerURL string `json:"server_url"` +} + +func ClusterHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + cluster := ClusterInfo{ + Name: "local", + Version: "v1.29.0", + Health: true, + ServerURL: "https://kubernetes.default.svc", + } + + json.NewEncoder(w).Encode(cluster) +} diff --git a/server/internal/api/handlers/health.go b/server/internal/api/handlers/health.go new file mode 100644 index 0000000..42a2f2a --- /dev/null +++ b/server/internal/api/handlers/health.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type HealthResponse struct { + Status string `json:"status"` +} + +func HealthHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(HealthResponse{Status: "ok"}) +} diff --git a/server/internal/api/handlers/namespaces.go b/server/internal/api/handlers/namespaces.go new file mode 100644 index 0000000..47d9db1 --- /dev/null +++ b/server/internal/api/handlers/namespaces.go @@ -0,0 +1,14 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +func NamespacesHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + namespaces := []string{"default", "kube-system", "kube-public"} + + json.NewEncoder(w).Encode(namespaces) +} diff --git a/server/internal/api/handlers/resources.go b/server/internal/api/handlers/resources.go new file mode 100644 index 0000000..8d2c34c --- /dev/null +++ b/server/internal/api/handlers/resources.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type ResourceResponse struct { + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace"` + UID string `json:"uid"` + Labels map[string]string `json:"labels,omitempty"` + CreatedAt string `json:"created_at"` +} + +func ResourcesHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Placeholder - will be implemented with actual K8s client + resources := []ResourceResponse{ + { + Kind: "Namespace", + Name: "default", + Namespace: "", + UID: "ns-default", + CreatedAt: "2024-01-01T00:00:00Z", + }, + } + + json.NewEncoder(w).Encode(resources) +} + +func ResourceHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Placeholder - will be implemented with actual K8s client + resource := ResourceResponse{ + Kind: "Pod", + Name: "example-pod", + Namespace: "default", + UID: "pod-example", + CreatedAt: "2024-01-01T00:00:00Z", + } + + json.NewEncoder(w).Encode(resource) +} diff --git a/server/internal/api/routes.go b/server/internal/api/routes.go new file mode 100644 index 0000000..fca5aba --- /dev/null +++ b/server/internal/api/routes.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "krates/server/internal/api/handlers" +) + +func SetupRoutes() http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc("/health", handlers.HealthHandler) + mux.HandleFunc("/cluster", handlers.ClusterHandler) + mux.HandleFunc("/resources", handlers.ResourcesHandler) + mux.HandleFunc("/resources/", handlers.ResourcesHandler) + mux.HandleFunc("/resource/", handlers.ResourceHandler) + mux.HandleFunc("/namespaces", handlers.NamespacesHandler) + + return mux +} diff --git a/server/internal/auth/token.go b/server/internal/auth/token.go new file mode 100644 index 0000000..0c6a541 --- /dev/null +++ b/server/internal/auth/token.go @@ -0,0 +1,74 @@ +package auth + +import ( + "context" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +type Claims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +type TokenManager struct { + secretKey string +} + +func NewTokenManager(secretKey string) *TokenManager { + return &TokenManager{ + secretKey: secretKey, + } +} + +func (tm *TokenManager) CreateToken(userID, username string) (string, error) { + claims := &Claims{ + UserID: userID, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(tm.secretKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +func (tm *TokenManager) ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(tm.secretKey), nil + }) + + if err != nil || !token.Valid { + return nil, err + } + + claims, ok := token.Claims.(*Claims) + if !ok { + return nil, jwt.ErrSignatureInvalid + } + + return claims, nil +} + +func ExtractToken(ctx context.Context) string { + // Extract token fromAuthorization header + authHeader := ctx.Value("Authorization") + if authHeader != nil { + header := authHeader.(string) + if strings.HasPrefix(header, "Bearer ") { + return strings.TrimPrefix(header, "Bearer ") + } + } + return "" +} diff --git a/server/internal/crdt/provider.go b/server/internal/crdt/provider.go new file mode 100644 index 0000000..a67dd7a --- /dev/null +++ b/server/internal/crdt/provider.go @@ -0,0 +1,24 @@ +package crdt + +import ( + "github.com/yjs/y-websocket/go/yws" +) + +type Provider struct { + doc *yws.Doc +} + +func NewProvider() *Provider { + doc := yws.NewDoc() + return &Provider{ + doc: doc, + } +} + +func (p *Provider) GetDoc() *yws.Doc { + return p.doc +} + +func (p *Provider) BroadcastUpdate(update []byte) { + // TODO: Broadcast Yjs update to all connected clients +} diff --git a/server/internal/k8s/client.go b/server/internal/k8s/client.go new file mode 100644 index 0000000..76e8473 --- /dev/null +++ b/server/internal/k8s/client.go @@ -0,0 +1,50 @@ +package k8s + +import ( + "context" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +type Client struct { + clientset *kubernetes.Clientset +} + +func NewClient() (*Client, error) { + config, err := rest.InClusterConfig() + if err != nil { + // Fall back to out-of-cluster config for local development + // config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(home, ".kube", "config")) + // if err != nil { + // return nil, err + // } + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return &Client{ + clientset: clientset, + }, nil +} + +func (c *Client) Clientset() *kubernetes.Clientset { + return c.clientset +} + +func (c *Client) GetPods(ctx context.Context, namespace string) ([]v1.Pod, error) { + pods, err := c.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + return pods.Items, nil +} + +func (c *Client) WatchPods(ctx context.Context, namespace string, resourceVersion string) (watch.Interface, error) { + return c.clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{ + ResourceVersion: resourceVersion, + }) +} diff --git a/server/internal/k8s/resources.go b/server/internal/k8s/resources.go new file mode 100644 index 0000000..3e7f5e1 --- /dev/null +++ b/server/internal/k8s/resources.go @@ -0,0 +1,28 @@ +package k8s + +import ( + "context" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (c *Client) GetPod(ctx context.Context, namespace, name string) (*v1.Pod, error) { + return c.clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) +} + +func (c *Client) GetDeployments(ctx context.Context, namespace string) ([]v1beta1.Deployment, error) { + deployments, err := c.clientset.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + return deployments.Items, nil +} + +func (c *Client) GetServices(ctx context.Context, namespace string) ([]v1.Service, error) { + services, err := c.clientset.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + return services.Items, nil +} diff --git a/server/internal/k8s/watch.go b/server/internal/k8s/watch.go new file mode 100644 index 0000000..413e296 --- /dev/null +++ b/server/internal/k8s/watch.go @@ -0,0 +1,24 @@ +package k8s + +import ( + "context" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type WatchStream struct { + Watcher watch.Interface + StopChan chan struct{} +} + +func (c *Client) WatchAllResources(ctx context.Context) (*WatchStream, error) { + // TODO: Implement watch for all resource types + // This will multiplex all watch streams into a single connection + + return nil, nil +} + +func (c *Client) WatchPodLogs(ctx context.Context, namespace, podName string, follow bool) (watch.Interface, error) { + return c.clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{}) +} diff --git a/server/internal/ws/logs.go b/server/internal/ws/logs.go new file mode 100644 index 0000000..439879f --- /dev/null +++ b/server/internal/ws/logs.go @@ -0,0 +1,20 @@ +package ws + +import ( + "github.com/gorilla/websocket" +) + +func LogsHandler(conn *websocket.Conn, r *http.Request) error { + // Extract pod and namespace from query params + pod := r.URL.Query().Get("pod") + namespace := r.URL.Query().Get("ns") + + if pod == "" || namespace == "" { + return websocket.CloseBadRequest + } + + // TODO: Implement logs streaming + // This will connect to k8s logs API and stream logs via WebSocket + + return nil +} diff --git a/server/internal/ws/manager.go b/server/internal/ws/manager.go new file mode 100644 index 0000000..3026600 --- /dev/null +++ b/server/internal/ws/manager.go @@ -0,0 +1,41 @@ +package ws + +import ( + "net/http" + + "github.com/gorilla/websocket" + "krates/server/internal/crdt" +) + +type HandlerFunc func(*websocket.Conn, *http.Request) error + +type WebSocketManager struct { + crdtProvider *crdt.Provider + upgrader *websocket.Upgrader +} + +func NewManager(crdtProvider *crdt.Provider) *WebSocketManager { + return &WebSocketManager{ + crdtProvider: crdtProvider, + upgrader: &websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + }, + } +} + +func (m *WebSocketManager) WithWebSocket(handler HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + conn, err := m.upgrader.Upgrade(w, r, nil) + if err != nil { + http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest) + return + } + defer conn.Close() + + if err := handler(conn, r); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} diff --git a/server/internal/ws/shell.go b/server/internal/ws/shell.go new file mode 100644 index 0000000..afd7eb8 --- /dev/null +++ b/server/internal/ws/shell.go @@ -0,0 +1,20 @@ +package ws + +import ( + "github.com/gorilla/websocket" +) + +func ShellHandler(conn *websocket.Conn, r *http.Request) error { + // Extract pod and namespace from query params + pod := r.URL.Query().Get("pod") + namespace := r.URL.Query().Get("ns") + + if pod == "" || namespace == "" { + return websocket.CloseBadRequest + } + + // TODO: Implement shell connection + // This will connect to k8s exec API and proxy WebSocket ↔ k8s stream + + return nil +} diff --git a/server/internal/ws/sync.go b/server/internal/ws/sync.go new file mode 100644 index 0000000..ad225fe --- /dev/null +++ b/server/internal/ws/sync.go @@ -0,0 +1,12 @@ +package ws + +import ( + "github.com/gorilla/websocket" +) + +func SyncHandler(conn *websocket.Conn, r *http.Request) error { + // TODO: Implement CRDT sync + // This will handle Yjs sync messages for shared workspace state + + return nil +} diff --git a/server/internal/ws/watch.go b/server/internal/ws/watch.go new file mode 100644 index 0000000..dfc3014 --- /dev/null +++ b/server/internal/ws/watch.go @@ -0,0 +1,12 @@ +package ws + +import ( + "github.com/gorilla/websocket" +) + +func WatchHandler(conn *websocket.Conn, r *http.Request) error { + // TODO: Implement resource watch streaming + // This will watch K8s resources and broadcast changes to clients + + return nil +}