Backend: API handlers, WebSocket manager, K8s client, CRDT, auth
This commit is contained in:
26
server/internal/api/handlers/cluster.go
Normal file
26
server/internal/api/handlers/cluster.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
15
server/internal/api/handlers/health.go
Normal file
15
server/internal/api/handlers/health.go
Normal file
@@ -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"})
|
||||||
|
}
|
||||||
14
server/internal/api/handlers/namespaces.go
Normal file
14
server/internal/api/handlers/namespaces.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
47
server/internal/api/handlers/resources.go
Normal file
47
server/internal/api/handlers/resources.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
20
server/internal/api/routes.go
Normal file
20
server/internal/api/routes.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
74
server/internal/auth/token.go
Normal file
74
server/internal/auth/token.go
Normal file
@@ -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 ""
|
||||||
|
}
|
||||||
24
server/internal/crdt/provider.go
Normal file
24
server/internal/crdt/provider.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
50
server/internal/k8s/client.go
Normal file
50
server/internal/k8s/client.go
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
28
server/internal/k8s/resources.go
Normal file
28
server/internal/k8s/resources.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
24
server/internal/k8s/watch.go
Normal file
24
server/internal/k8s/watch.go
Normal file
@@ -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{})
|
||||||
|
}
|
||||||
20
server/internal/ws/logs.go
Normal file
20
server/internal/ws/logs.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
41
server/internal/ws/manager.go
Normal file
41
server/internal/ws/manager.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
server/internal/ws/shell.go
Normal file
20
server/internal/ws/shell.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
12
server/internal/ws/sync.go
Normal file
12
server/internal/ws/sync.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
12
server/internal/ws/watch.go
Normal file
12
server/internal/ws/watch.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user