From d0c96e0fd4b6e07c53cdb9d8c7c69bf3b83301fb Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 11 May 2025 15:01:02 +0530 Subject: [PATCH] initial setup for k8s mcp server --- services/mcp/kubernetes/.gitignore | 36 ++++ services/mcp/kubernetes/Dockerfile | 32 ++++ services/mcp/kubernetes/README.md | 104 +++++++++++ services/mcp/kubernetes/cmd/server/main.go | 92 ++++++++++ services/mcp/kubernetes/config/config.yaml | 25 +++ .../deploy/kubernetes/configmap.yaml | 29 +++ .../deploy/kubernetes/deployment.yaml | 57 ++++++ .../deploy/kubernetes/namespace.yaml | 6 + .../kubernetes/deploy/kubernetes/rbac.yaml | 35 ++++ .../kubernetes/deploy/kubernetes/service.yaml | 18 ++ services/mcp/kubernetes/go.mod | 49 +++++ services/mcp/kubernetes/go.sum | 158 ++++++++++++++++ services/mcp/kubernetes/pkg/api/server.go | 153 ++++++++++++++++ services/mcp/kubernetes/pkg/auth/auth.go | 135 ++++++++++++++ services/mcp/kubernetes/pkg/auth/context.go | 37 ++++ .../mcp/kubernetes/pkg/controller/manager.go | 67 +++++++ .../pkg/controller/service_controller.go | 168 ++++++++++++++++++ services/mcp/kubernetes/pkg/k8s/client.go | 44 +++++ services/mcp/kubernetes/pkg/model/types.go | 116 ++++++++++++ 19 files changed, 1361 insertions(+) create mode 100644 services/mcp/kubernetes/.gitignore create mode 100644 services/mcp/kubernetes/Dockerfile create mode 100644 services/mcp/kubernetes/README.md create mode 100644 services/mcp/kubernetes/cmd/server/main.go create mode 100644 services/mcp/kubernetes/config/config.yaml create mode 100644 services/mcp/kubernetes/deploy/kubernetes/configmap.yaml create mode 100644 services/mcp/kubernetes/deploy/kubernetes/deployment.yaml create mode 100644 services/mcp/kubernetes/deploy/kubernetes/namespace.yaml create mode 100644 services/mcp/kubernetes/deploy/kubernetes/rbac.yaml create mode 100644 services/mcp/kubernetes/deploy/kubernetes/service.yaml create mode 100644 services/mcp/kubernetes/go.mod create mode 100644 services/mcp/kubernetes/go.sum create mode 100644 services/mcp/kubernetes/pkg/api/server.go create mode 100644 services/mcp/kubernetes/pkg/auth/auth.go create mode 100644 services/mcp/kubernetes/pkg/auth/context.go create mode 100644 services/mcp/kubernetes/pkg/controller/manager.go create mode 100644 services/mcp/kubernetes/pkg/controller/service_controller.go create mode 100644 services/mcp/kubernetes/pkg/k8s/client.go create mode 100644 services/mcp/kubernetes/pkg/model/types.go diff --git a/services/mcp/kubernetes/.gitignore b/services/mcp/kubernetes/.gitignore new file mode 100644 index 0000000..59d7915 --- /dev/null +++ b/services/mcp/kubernetes/.gitignore @@ -0,0 +1,36 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work + +# Dependency directories +/vendor/ + +# Build outputs +/bin/ +/dist/ + +# IDE and editor directories +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db + +# Local configuration files +*.local.yaml +*.env \ No newline at end of file diff --git a/services/mcp/kubernetes/Dockerfile b/services/mcp/kubernetes/Dockerfile new file mode 100644 index 0000000..03aeddb --- /dev/null +++ b/services/mcp/kubernetes/Dockerfile @@ -0,0 +1,32 @@ +FROM golang:1.22-alpine AS builder + +WORKDIR /app + +# Copy the go module files first to leverage Docker cache +COPY go.mod ./ +COPY go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy the source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mcp-server ./cmd/server + +# Use distroless for minimal image +FROM gcr.io/distroless/static:nonroot + +WORKDIR /app + +# Copy the binary from the builder stage +COPY --from=builder /app/mcp-server . +COPY --from=builder /app/config /app/config + +# Run as non-root user +USER nonroot:nonroot + +EXPOSE 8080 8081 + +ENTRYPOINT ["/app/mcp-server"] \ No newline at end of file diff --git a/services/mcp/kubernetes/README.md b/services/mcp/kubernetes/README.md new file mode 100644 index 0000000..07f5923 --- /dev/null +++ b/services/mcp/kubernetes/README.md @@ -0,0 +1,104 @@ +# Kubernetes MCP Server + +A Management Control Plane (MCP) server for Kubernetes that provides APIs for managing services. + +## Overview + +The MCP server is a Go-based application that interacts with Kubernetes to provide a higher-level API for managing services. It serves as an abstraction layer between users and Kubernetes, making it easier to deploy and manage applications. + +## Features + +- RESTful API for service management +- Authentication with JWT and API keys +- Kubernetes controller for service reconciliation +- Metrics and health monitoring +- Deployment as a Kubernetes application + +## Project Structure + +``` +. +├── cmd/ # Command-line applications +│ └── server/ # MCP server entry point +├── pkg/ # Library packages +│ ├── api/ # API server and handlers +│ ├── auth/ # Authentication and authorization +│ ├── controller/ # Kubernetes controllers +│ ├── k8s/ # Kubernetes client utilities +│ └── model/ # Data models +├── config/ # Configuration files +├── deploy/ # Deployment manifests +│ └── kubernetes/ # Kubernetes deployment manifests +└── Dockerfile # Container image definition +``` + +## Getting Started + +### Prerequisites + +- Go 1.22 or higher +- Access to a Kubernetes cluster +- kubectl configured to access your cluster + +### Building + +```bash +# Build the server +go build -o mcp-server ./cmd/server + +# Run the server +./mcp-server +``` + +### Running with Docker + +```bash +# Build the Docker image +docker build -t infragpt/mcp-server:latest . + +# Run the container +docker run -p 8081:8081 -p 8080:8080 infragpt/mcp-server:latest +``` + +### Deploying to Kubernetes + +```bash +# Create namespace +kubectl apply -f deploy/kubernetes/namespace.yaml + +# Create RBAC resources +kubectl apply -f deploy/kubernetes/rbac.yaml + +# Create ConfigMap +kubectl apply -f deploy/kubernetes/configmap.yaml + +# Deploy the server +kubectl apply -f deploy/kubernetes/deployment.yaml +kubectl apply -f deploy/kubernetes/service.yaml +``` + +## API Endpoints + +- `GET /api/v1/health` - Health check +- `GET /api/v1/services` - List services +- `POST /api/v1/services` - Create a service + +## Configuration + +Configuration is loaded from `config/config.yaml`. See the example configuration for details. + +## Development + +### Running Tests + +```bash +go test ./... +``` + +### Code Style + +This project follows the standard Go style guidelines and uses `gofmt` for formatting. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/services/mcp/kubernetes/cmd/server/main.go b/services/mcp/kubernetes/cmd/server/main.go new file mode 100644 index 0000000..4ce98f5 --- /dev/null +++ b/services/mcp/kubernetes/cmd/server/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/infragpt/services/mcp/kubernetes/pkg/api" + "github.com/infragpt/services/mcp/kubernetes/pkg/controller" + "github.com/infragpt/services/mcp/kubernetes/pkg/k8s" +) + +func main() { + var ( + kubeconfig string + masterURL string + metricsAddr string + enableLeaderElection bool + apiAddr string + ) + + flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file") + flag.StringVar(&masterURL, "master", "", "URL to Kubernetes API server") + flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "Address for serving metrics") + flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election") + flag.StringVar(&apiAddr, "api-addr", ":8081", "Address for serving API") + flag.Parse() + + // Set up logging + logger := log.New(os.Stdout, "MCP-SERVER: ", log.LstdFlags) + + // Initialize Kubernetes client + clientConfig, err := k8s.NewClientConfig(kubeconfig, masterURL) + if err != nil { + logger.Fatalf("Error building kubernetes clientset: %s", err.Error()) + } + + // Create context with cancellation + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Set up controllers + controllerManager, err := controller.NewManager(ctx, clientConfig, controller.Options{ + MetricsAddr: metricsAddr, + EnableLeaderElection: enableLeaderElection, + LeaderElectionID: "mcp-controller-lock", + }) + if err != nil { + logger.Fatalf("Unable to set up controller manager: %s", err.Error()) + } + + // Start the API server + apiServer := api.NewServer(apiAddr, clientConfig) + go func() { + if err := apiServer.Start(ctx); err != nil { + logger.Fatalf("Error starting API server: %s", err.Error()) + } + }() + + // Handle shutdown gracefully + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) + + // Start the controller manager + go func() { + if err := controllerManager.Start(); err != nil { + logger.Fatalf("Error starting controller manager: %s", err.Error()) + } + }() + + logger.Printf("MCP server started, waiting for signal to exit") + + // Wait for shutdown signal + <-signalCh + logger.Println("Received shutdown signal, shutting down gracefully...") + + // Give controllers time to shutdown gracefully + cancelCtx, cancelFunc := context.WithTimeout(ctx, 5*time.Second) + defer cancelFunc() + + if err := apiServer.Shutdown(cancelCtx); err != nil { + logger.Printf("Error during API server shutdown: %s", err.Error()) + } + + cancel() // Signal controllers to shutdown + + logger.Println("Shutdown complete") +} \ No newline at end of file diff --git a/services/mcp/kubernetes/config/config.yaml b/services/mcp/kubernetes/config/config.yaml new file mode 100644 index 0000000..6993ec6 --- /dev/null +++ b/services/mcp/kubernetes/config/config.yaml @@ -0,0 +1,25 @@ +api: + addr: ":8081" + +metrics: + addr: ":8080" + +auth: + jwtSecret: "replace-with-real-secret-in-production" + tokenExpiration: "24h" + apiKeys: + admin: "replace-with-real-api-key-in-production" + +kubernetes: + inCluster: true + # Uncomment these if not running in cluster + # kubeconfig: "" + # masterURL: "" + +controller: + enableLeaderElection: true + leaderElectionID: "mcp-controller-lock" + +logging: + level: "info" + format: "json" \ No newline at end of file diff --git a/services/mcp/kubernetes/deploy/kubernetes/configmap.yaml b/services/mcp/kubernetes/deploy/kubernetes/configmap.yaml new file mode 100644 index 0000000..988b96e --- /dev/null +++ b/services/mcp/kubernetes/deploy/kubernetes/configmap.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mcp-server-config + namespace: infragpt +data: + config.yaml: | + api: + addr: ":8081" + + metrics: + addr: ":8080" + + auth: + jwtSecret: "${JWT_SECRET}" + tokenExpiration: "24h" + apiKeys: + admin: "${API_KEY}" + + kubernetes: + inCluster: true + + controller: + enableLeaderElection: true + leaderElectionID: "mcp-controller-lock" + + logging: + level: "info" + format: "json" \ No newline at end of file diff --git a/services/mcp/kubernetes/deploy/kubernetes/deployment.yaml b/services/mcp/kubernetes/deploy/kubernetes/deployment.yaml new file mode 100644 index 0000000..ed943a5 --- /dev/null +++ b/services/mcp/kubernetes/deploy/kubernetes/deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mcp-server + namespace: infragpt + labels: + app: mcp-server +spec: + replicas: 1 + selector: + matchLabels: + app: mcp-server + template: + metadata: + labels: + app: mcp-server + spec: + serviceAccountName: mcp-server + containers: + - name: mcp-server + image: infragpt/mcp-server:latest + imagePullPolicy: Always + args: + - "--metrics-addr=:8080" + - "--api-addr=:8081" + - "--enable-leader-election=true" + ports: + - name: metrics + containerPort: 8080 + - name: api + containerPort: 8081 + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "100m" + memory: "128Mi" + livenessProbe: + httpGet: + path: /api/v1/health + port: api + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /api/v1/health + port: api + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: config + mountPath: /app/config + volumes: + - name: config + configMap: + name: mcp-server-config \ No newline at end of file diff --git a/services/mcp/kubernetes/deploy/kubernetes/namespace.yaml b/services/mcp/kubernetes/deploy/kubernetes/namespace.yaml new file mode 100644 index 0000000..4051292 --- /dev/null +++ b/services/mcp/kubernetes/deploy/kubernetes/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: infragpt + labels: + name: infragpt \ No newline at end of file diff --git a/services/mcp/kubernetes/deploy/kubernetes/rbac.yaml b/services/mcp/kubernetes/deploy/kubernetes/rbac.yaml new file mode 100644 index 0000000..bc9f3a4 --- /dev/null +++ b/services/mcp/kubernetes/deploy/kubernetes/rbac.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mcp-server + namespace: infragpt + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mcp-server +rules: +- apiGroups: [""] + resources: ["pods", "services", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["apps"] + resources: ["deployments", "statefulsets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mcp-server +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: mcp-server +subjects: +- kind: ServiceAccount + name: mcp-server + namespace: infragpt \ No newline at end of file diff --git a/services/mcp/kubernetes/deploy/kubernetes/service.yaml b/services/mcp/kubernetes/deploy/kubernetes/service.yaml new file mode 100644 index 0000000..d3ebf10 --- /dev/null +++ b/services/mcp/kubernetes/deploy/kubernetes/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: mcp-server + namespace: infragpt + labels: + app: mcp-server +spec: + selector: + app: mcp-server + ports: + - name: api + port: 8081 + targetPort: api + - name: metrics + port: 8080 + targetPort: metrics + type: ClusterIP \ No newline at end of file diff --git a/services/mcp/kubernetes/go.mod b/services/mcp/kubernetes/go.mod new file mode 100644 index 0000000..29d52ee --- /dev/null +++ b/services/mcp/kubernetes/go.mod @@ -0,0 +1,49 @@ +module github.com/infragpt/services/mcp/kubernetes + +go 1.22 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/services/mcp/kubernetes/go.sum b/services/mcp/kubernetes/go.sum new file mode 100644 index 0000000..81db2ff --- /dev/null +++ b/services/mcp/kubernetes/go.sum @@ -0,0 +1,158 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/services/mcp/kubernetes/pkg/api/server.go b/services/mcp/kubernetes/pkg/api/server.go new file mode 100644 index 0000000..b64a016 --- /dev/null +++ b/services/mcp/kubernetes/pkg/api/server.go @@ -0,0 +1,153 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/infragpt/services/mcp/kubernetes/pkg/k8s" + "github.com/infragpt/services/mcp/kubernetes/pkg/model" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Server represents the API server +type Server struct { + addr string + clientConfig *k8s.ClientConfig + server *http.Server + logger *log.Logger +} + +// NewServer creates a new API server instance +func NewServer(addr string, clientConfig *k8s.ClientConfig) *Server { + logger := log.New(os.Stdout, "API-SERVER: ", log.LstdFlags) + + return &Server{ + addr: addr, + clientConfig: clientConfig, + logger: logger, + } +} + +// Start starts the API server +func (s *Server) Start(ctx context.Context) error { + mux := http.NewServeMux() + + // Register API endpoints + mux.HandleFunc("/api/v1/health", s.healthHandler) + mux.HandleFunc("/api/v1/services", s.servicesHandler) + + s.server = &http.Server{ + Addr: s.addr, + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 30 * time.Second, + } + + s.logger.Printf("Starting API server on %s", s.addr) + + if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return fmt.Errorf("error starting server: %w", err) + } + + return nil +} + +// Shutdown gracefully shuts down the API server +func (s *Server) Shutdown(ctx context.Context) error { + s.logger.Println("Shutting down API server...") + return s.server.Shutdown(ctx) +} + +// healthHandler handles health check requests +func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + response := map[string]string{ + "status": "ok", + "time": time.Now().Format(time.RFC3339), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// servicesHandler handles service-related requests +func (s *Server) servicesHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + s.listServices(w, r) + case http.MethodPost: + s.createService(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// listServices returns a list of all services +func (s *Server) listServices(w http.ResponseWriter, r *http.Request) { + // In a real implementation, this would fetch services from Kubernetes + // For now, return a sample service + services := []model.MCPService{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "sample-service", + Namespace: "default", + }, + Spec: model.MCPServiceSpec{ + Type: "web", + Version: "1.0.0", + Replicas: 2, + Image: "nginx:latest", + }, + Status: model.MCPServiceStatus{ + Phase: model.ServiceRunning, + AvailableReplicas: 2, + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(services) +} + +// createService creates a new service +func (s *Server) createService(w http.ResponseWriter, r *http.Request) { + var service model.MCPService + + if err := json.NewDecoder(r.Body).Decode(&service); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Validate service + if service.Spec.Type == "" || service.Spec.Image == "" { + http.Error(w, "Service type and image are required", http.StatusBadRequest) + return + } + + // Set default values if not provided + if service.Spec.Replicas <= 0 { + service.Spec.Replicas = 1 + } + + // In a real implementation, this would create the service in Kubernetes + // Mock a successful creation + service.Status = model.MCPServiceStatus{ + Phase: model.ServicePending, + AvailableReplicas: 0, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(service) +} \ No newline at end of file diff --git a/services/mcp/kubernetes/pkg/auth/auth.go b/services/mcp/kubernetes/pkg/auth/auth.go new file mode 100644 index 0000000..dae9219 --- /dev/null +++ b/services/mcp/kubernetes/pkg/auth/auth.go @@ -0,0 +1,135 @@ +package auth + +import ( + "crypto/subtle" + "fmt" + "net/http" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" +) + +// AuthConfig holds authentication configuration +type AuthConfig struct { + // JWTSecret is the secret key used to sign JWTs + JWTSecret string + + // APIKeys is a map of API key IDs to API keys + APIKeys map[string]string + + // TokenExpiration is the duration for which a token is valid + TokenExpiration time.Duration +} + +// TokenClaims represents JWT claims +type TokenClaims struct { + jwt.StandardClaims + UserID string `json:"userId"` + Username string `json:"username"` + Roles []string `json:"roles"` +} + +// NewAuthConfig creates a new authentication configuration +func NewAuthConfig(jwtSecret string, apiKeys map[string]string, tokenExpiration time.Duration) *AuthConfig { + return &AuthConfig{ + JWTSecret: jwtSecret, + APIKeys: apiKeys, + TokenExpiration: tokenExpiration, + } +} + +// Authenticator is a middleware for authenticating requests +func (c *AuthConfig) Authenticator(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Skip authentication for health endpoint + if r.URL.Path == "/api/v1/health" { + next.ServeHTTP(w, r) + return + } + + // Check for API key authentication + if apiKey := r.Header.Get("X-API-Key"); apiKey != "" { + if c.validateAPIKey(apiKey) { + next.ServeHTTP(w, r) + return + } + } + + // Check for JWT authentication + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Unauthorized: Missing authentication", http.StatusUnauthorized) + return + } + + // Extract token from header + tokenStr := strings.TrimPrefix(authHeader, "Bearer ") + if tokenStr == authHeader { + http.Error(w, "Unauthorized: Invalid token format", http.StatusUnauthorized) + return + } + + // Validate token + claims, err := c.validateToken(tokenStr) + if err != nil { + http.Error(w, fmt.Sprintf("Unauthorized: %s", err.Error()), http.StatusUnauthorized) + return + } + + // Set claims in context for later use + ctx := r.Context() + ctx = ContextWithClaims(ctx, claims) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// validateAPIKey validates an API key +func (c *AuthConfig) validateAPIKey(apiKey string) bool { + for _, key := range c.APIKeys { + if subtle.ConstantTimeCompare([]byte(apiKey), []byte(key)) == 1 { + return true + } + } + return false +} + +// validateToken validates a JWT token +func (c *AuthConfig) validateToken(tokenStr string) (*TokenClaims, error) { + claims := &TokenClaims{} + + token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(c.JWTSecret), nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, fmt.Errorf("invalid token") + } + + return claims, nil +} + +// GenerateToken generates a new JWT token +func (c *AuthConfig) GenerateToken(userID, username string, roles []string) (string, error) { + claims := TokenClaims{ + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(c.TokenExpiration).Unix(), + IssuedAt: time.Now().Unix(), + Issuer: "mcp-server", + }, + UserID: userID, + Username: username, + Roles: roles, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(c.JWTSecret)) +} \ No newline at end of file diff --git a/services/mcp/kubernetes/pkg/auth/context.go b/services/mcp/kubernetes/pkg/auth/context.go new file mode 100644 index 0000000..3d9e86d --- /dev/null +++ b/services/mcp/kubernetes/pkg/auth/context.go @@ -0,0 +1,37 @@ +package auth + +import ( + "context" +) + +type contextKey string + +const ( + claimsContextKey contextKey = "claims" +) + +// ContextWithClaims adds token claims to the context +func ContextWithClaims(ctx context.Context, claims *TokenClaims) context.Context { + return context.WithValue(ctx, claimsContextKey, claims) +} + +// ClaimsFromContext extracts token claims from the context +func ClaimsFromContext(ctx context.Context) (*TokenClaims, bool) { + claims, ok := ctx.Value(claimsContextKey).(*TokenClaims) + return claims, ok +} + +// HasRole checks if the claims in the context have a specific role +func HasRole(ctx context.Context, role string) bool { + claims, ok := ClaimsFromContext(ctx) + if !ok { + return false + } + + for _, r := range claims.Roles { + if r == role { + return true + } + } + return false +} \ No newline at end of file diff --git a/services/mcp/kubernetes/pkg/controller/manager.go b/services/mcp/kubernetes/pkg/controller/manager.go new file mode 100644 index 0000000..5b6a148 --- /dev/null +++ b/services/mcp/kubernetes/pkg/controller/manager.go @@ -0,0 +1,67 @@ +package controller + +import ( + "context" + "fmt" + + "github.com/infragpt/services/mcp/kubernetes/pkg/k8s" +) + +// Options contains options for the controller manager +type Options struct { + MetricsAddr string + EnableLeaderElection bool + LeaderElectionID string +} + +// Manager manages a set of controllers +type Manager struct { + clientConfig *k8s.ClientConfig + options Options + controllers []Controller + ctx context.Context +} + +// Controller defines the interface for a controller +type Controller interface { + Start(ctx context.Context) error + Name() string +} + +// NewManager creates a new controller manager +func NewManager(ctx context.Context, clientConfig *k8s.ClientConfig, options Options) (*Manager, error) { + manager := &Manager{ + clientConfig: clientConfig, + options: options, + ctx: ctx, + } + + // Register controllers + serviceController, err := NewServiceController(clientConfig) + if err != nil { + return nil, fmt.Errorf("failed to create service controller: %w", err) + } + + manager.RegisterController(serviceController) + + return manager, nil +} + +// RegisterController registers a new controller +func (m *Manager) RegisterController(controller Controller) { + m.controllers = append(m.controllers, controller) +} + +// Start starts all controllers +func (m *Manager) Start() error { + for _, controller := range m.controllers { + go func(c Controller) { + if err := c.Start(m.ctx); err != nil { + fmt.Printf("Error starting controller %s: %v\n", c.Name(), err) + } + }(controller) + } + + <-m.ctx.Done() + return nil +} \ No newline at end of file diff --git a/services/mcp/kubernetes/pkg/controller/service_controller.go b/services/mcp/kubernetes/pkg/controller/service_controller.go new file mode 100644 index 0000000..6694eda --- /dev/null +++ b/services/mcp/kubernetes/pkg/controller/service_controller.go @@ -0,0 +1,168 @@ +package controller + +import ( + "context" + "fmt" + "log" + "os" + "sync" + "time" + + "github.com/infragpt/services/mcp/kubernetes/pkg/k8s" + "github.com/infragpt/services/mcp/kubernetes/pkg/model" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ServiceController manages MCP services +type ServiceController struct { + clientConfig *k8s.ClientConfig + services map[string]model.MCPService + mutex sync.RWMutex + logger *log.Logger +} + +// NewServiceController creates a new service controller +func NewServiceController(clientConfig *k8s.ClientConfig) (*ServiceController, error) { + logger := log.New(os.Stdout, "SERVICE-CONTROLLER: ", log.LstdFlags) + + return &ServiceController{ + clientConfig: clientConfig, + services: make(map[string]model.MCPService), + logger: logger, + }, nil +} + +// Name returns the controller name +func (c *ServiceController) Name() string { + return "service-controller" +} + +// Start starts the service controller +func (c *ServiceController) Start(ctx context.Context) error { + c.logger.Println("Starting service controller") + + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + // Initial reconciliation + if err := c.reconcile(ctx); err != nil { + c.logger.Printf("Initial reconciliation failed: %v", err) + } + + for { + select { + case <-ctx.Done(): + c.logger.Println("Shutting down service controller") + return nil + case <-ticker.C: + if err := c.reconcile(ctx); err != nil { + c.logger.Printf("Reconciliation failed: %v", err) + } + } + } +} + +// reconcile reconciles the desired state with the actual state +func (c *ServiceController) reconcile(ctx context.Context) error { + c.logger.Println("Reconciling services") + + // In a real implementation, this would: + // 1. List MCP services from a custom resource definition (CRD) + // 2. For each service, ensure the corresponding Kubernetes resources exist + // 3. Update status based on the actual state + + // Mock reconciliation for demonstration + c.mutex.Lock() + defer c.mutex.Unlock() + + for key, service := range c.services { + // Simulate reconciliation by updating status + + // Check if service needs to be deployed + if service.Status.Phase == model.ServicePending { + c.logger.Printf("Deploying service %s", service.ObjectMeta.Name) + + service.Status.Phase = model.ServiceDeploying + service.Status.Conditions = append(service.Status.Conditions, model.ServiceCondition{ + Type: model.ServiceProgressing, + Status: model.ConditionTrue, + LastUpdateTime: metav1.Now(), + LastTransitionTime: metav1.Now(), + Reason: "Deploying", + Message: fmt.Sprintf("Deploying service %s", service.ObjectMeta.Name), + }) + + c.services[key] = service + + // Simulate deployment time + time.Sleep(2 * time.Second) + + // Update status to running after "deployment" + service.Status.Phase = model.ServiceRunning + service.Status.AvailableReplicas = service.Spec.Replicas + service.Status.Conditions = append(service.Status.Conditions, model.ServiceCondition{ + Type: model.ServiceAvailable, + Status: model.ConditionTrue, + LastUpdateTime: metav1.Now(), + LastTransitionTime: metav1.Now(), + Reason: "ServiceDeployed", + Message: fmt.Sprintf("Service %s successfully deployed", service.ObjectMeta.Name), + }) + + c.services[key] = service + c.logger.Printf("Service %s deployed successfully", service.ObjectMeta.Name) + } + } + + return nil +} + +// AddService adds a service to be managed by the controller +func (c *ServiceController) AddService(service model.MCPService) { + c.mutex.Lock() + defer c.mutex.Unlock() + + key := fmt.Sprintf("%s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name) + c.services[key] = service + + c.logger.Printf("Added service %s to controller", key) +} + +// GetService gets a service by name and namespace +func (c *ServiceController) GetService(namespace, name string) (model.MCPService, bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + key := fmt.Sprintf("%s/%s", namespace, name) + service, exists := c.services[key] + return service, exists +} + +// ListServices lists all services +func (c *ServiceController) ListServices() []model.MCPService { + c.mutex.RLock() + defer c.mutex.RUnlock() + + services := make([]model.MCPService, 0, len(c.services)) + for _, service := range c.services { + services = append(services, service) + } + + return services +} + +// DeleteService deletes a service +func (c *ServiceController) DeleteService(namespace, name string) bool { + c.mutex.Lock() + defer c.mutex.Unlock() + + key := fmt.Sprintf("%s/%s", namespace, name) + _, exists := c.services[key] + if exists { + delete(c.services, key) + c.logger.Printf("Deleted service %s", key) + } + + return exists +} \ No newline at end of file diff --git a/services/mcp/kubernetes/pkg/k8s/client.go b/services/mcp/kubernetes/pkg/k8s/client.go new file mode 100644 index 0000000..8c6d8b2 --- /dev/null +++ b/services/mcp/kubernetes/pkg/k8s/client.go @@ -0,0 +1,44 @@ +package k8s + +import ( + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// ClientConfig provides access to the Kubernetes API +type ClientConfig struct { + RestConfig *rest.Config + Clientset *kubernetes.Clientset +} + +// NewClientConfig creates a new Kubernetes client configuration +func NewClientConfig(kubeconfigPath, masterURL string) (*ClientConfig, error) { + var config *rest.Config + var err error + + // Try to use in-cluster config if no kubeconfig is provided + if kubeconfigPath == "" { + config, err = rest.InClusterConfig() + if err != nil { + return nil, err + } + } else { + // Use kubeconfig file + config, err = clientcmd.BuildConfigFromFlags(masterURL, kubeconfigPath) + if err != nil { + return nil, err + } + } + + // Create the clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return &ClientConfig{ + RestConfig: config, + Clientset: clientset, + }, nil +} \ No newline at end of file diff --git a/services/mcp/kubernetes/pkg/model/types.go b/services/mcp/kubernetes/pkg/model/types.go new file mode 100644 index 0000000..34dc9ca --- /dev/null +++ b/services/mcp/kubernetes/pkg/model/types.go @@ -0,0 +1,116 @@ +package model + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MCPService represents a service managed by the MCP +type MCPService struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MCPServiceSpec `json:"spec,omitempty"` + Status MCPServiceStatus `json:"status,omitempty"` +} + +// MCPServiceSpec defines the desired state of an MCP service +type MCPServiceSpec struct { + // Type defines the service type + Type string `json:"type"` + + // Version defines the service version + Version string `json:"version"` + + // Replicas is the desired number of replicas + Replicas int32 `json:"replicas"` + + // Image is the container image to use + Image string `json:"image"` + + // Resources defines the resource requirements + Resources ResourceRequirements `json:"resources,omitempty"` + + // Config holds service-specific configuration + Config map[string]string `json:"config,omitempty"` +} + +// ResourceRequirements defines resource requirements for the service +type ResourceRequirements struct { + CPU string `json:"cpu,omitempty"` + Memory string `json:"memory,omitempty"` +} + +// MCPServiceStatus defines the observed state of an MCP service +type MCPServiceStatus struct { + // Phase represents the current lifecycle phase of the service + Phase ServicePhase `json:"phase"` + + // AvailableReplicas is the number of available replicas + AvailableReplicas int32 `json:"availableReplicas"` + + // Conditions represents the latest available observations of the service's state + Conditions []ServiceCondition `json:"conditions,omitempty"` +} + +// ServicePhase is a label for the condition of a service at the current time +type ServicePhase string + +const ( + // ServicePending means the service has been accepted by the system, but deployment is pending + ServicePending ServicePhase = "Pending" + + // ServiceDeploying means the deployment is in progress + ServiceDeploying ServicePhase = "Deploying" + + // ServiceRunning means the service is operational + ServiceRunning ServicePhase = "Running" + + // ServiceFailed means the service is not operational + ServiceFailed ServicePhase = "Failed" +) + +// ServiceCondition describes the state of a service at a certain point +type ServiceCondition struct { + // Type of service condition + Type ServiceConditionType `json:"type"` + + // Status of the condition, one of True, False, Unknown + Status ConditionStatus `json:"status"` + + // Last time the condition was updated + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` + + // Last time the condition transitioned from one status to another + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition + Reason string `json:"reason,omitempty"` + + // A human-readable message indicating details about the transition + Message string `json:"message,omitempty"` +} + +// ServiceConditionType is a valid value for ServiceCondition.Type +type ServiceConditionType string + +const ( + // ServiceAvailable means the service is available and ready to accept requests + ServiceAvailable ServiceConditionType = "Available" + + // ServiceProgressing means the deployment is progressing + ServiceProgressing ServiceConditionType = "Progressing" +) + +// ConditionStatus is the status of a condition +type ConditionStatus string + +const ( + // ConditionTrue means a condition is true + ConditionTrue ConditionStatus = "True" + + // ConditionFalse means a condition is false + ConditionFalse ConditionStatus = "False" + + // ConditionUnknown means the system cannot determine the status of a condition + ConditionUnknown ConditionStatus = "Unknown" +) \ No newline at end of file