From 658d150fd6c28edd6c5ebd54b61ee10c8210f149 Mon Sep 17 00:00:00 2001 From: manuelfdg Date: Wed, 16 Apr 2025 00:27:19 +0000 Subject: [PATCH 01/18] add hpa/resources requests and limits --- code/kubernetes/grpc-gateway/deployment.yaml | 7 +++++++ code/kubernetes/hpa/comment-service-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/hpa/community-service-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/hpa/grpc-gateway-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/hpa/popular-service-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/hpa/search-service-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/hpa/thread-service-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/hpa/vote-service-hpa.yaml | 18 ++++++++++++++++++ code/kubernetes/mongo/deployment.yaml | 7 +++++++ code/kubernetes/scripts/deploy.sh | 5 ++++- .../services/comment-service/deployment.yaml | 7 +++++++ .../services/community-service/deployment.yaml | 7 +++++++ .../services/db-service/deployment.yaml | 7 +++++++ .../services/popular-service/deployment.yaml | 7 +++++++ .../services/search-service/deployment.yaml | 7 +++++++ .../services/thread-service/deployment.yaml | 7 +++++++ .../services/vote-service/deployment.yaml | 7 +++++++ 17 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 code/kubernetes/hpa/comment-service-hpa.yaml create mode 100644 code/kubernetes/hpa/community-service-hpa.yaml create mode 100644 code/kubernetes/hpa/grpc-gateway-hpa.yaml create mode 100644 code/kubernetes/hpa/popular-service-hpa.yaml create mode 100644 code/kubernetes/hpa/search-service-hpa.yaml create mode 100644 code/kubernetes/hpa/thread-service-hpa.yaml create mode 100644 code/kubernetes/hpa/vote-service-hpa.yaml diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 75515f1..02885f1 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 8080 + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" env: - name: GRPC_GATEWAY_PORT valueFrom: diff --git a/code/kubernetes/hpa/comment-service-hpa.yaml b/code/kubernetes/hpa/comment-service-hpa.yaml new file mode 100644 index 0000000..8826295 --- /dev/null +++ b/code/kubernetes/hpa/comment-service-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: comment-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: comment-service + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/community-service-hpa.yaml b/code/kubernetes/hpa/community-service-hpa.yaml new file mode 100644 index 0000000..22aca1d --- /dev/null +++ b/code/kubernetes/hpa/community-service-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: community-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: community-service + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/grpc-gateway-hpa.yaml b/code/kubernetes/hpa/grpc-gateway-hpa.yaml new file mode 100644 index 0000000..483318e --- /dev/null +++ b/code/kubernetes/hpa/grpc-gateway-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: grpc-gateway-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: grpc-gateway + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/popular-service-hpa.yaml b/code/kubernetes/hpa/popular-service-hpa.yaml new file mode 100644 index 0000000..b08321b --- /dev/null +++ b/code/kubernetes/hpa/popular-service-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: popular-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: popular-service + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/search-service-hpa.yaml b/code/kubernetes/hpa/search-service-hpa.yaml new file mode 100644 index 0000000..adfb5d1 --- /dev/null +++ b/code/kubernetes/hpa/search-service-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: search-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: search-service + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/thread-service-hpa.yaml b/code/kubernetes/hpa/thread-service-hpa.yaml new file mode 100644 index 0000000..333d0c2 --- /dev/null +++ b/code/kubernetes/hpa/thread-service-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: thread-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: thread-service + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/vote-service-hpa.yaml b/code/kubernetes/hpa/vote-service-hpa.yaml new file mode 100644 index 0000000..f273f07 --- /dev/null +++ b/code/kubernetes/hpa/vote-service-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: vote-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: vote-service + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/mongo/deployment.yaml b/code/kubernetes/mongo/deployment.yaml index 6f788ff..e3e1739 100644 --- a/code/kubernetes/mongo/deployment.yaml +++ b/code/kubernetes/mongo/deployment.yaml @@ -17,6 +17,13 @@ spec: image: mongo:latest ports: - containerPort: 27017 + resources: + requests: + cpu: "300m" + memory: "512Mi" + limits: + cpu: "700m" + memory: "1Gi" env: - name: MONGO_INITDB_DATABASE valueFrom: diff --git a/code/kubernetes/scripts/deploy.sh b/code/kubernetes/scripts/deploy.sh index d8117c1..2772453 100644 --- a/code/kubernetes/scripts/deploy.sh +++ b/code/kubernetes/scripts/deploy.sh @@ -59,4 +59,7 @@ for SERVICE in "${SERVICES[@]}"; do done # gRPC Gateway -kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ \ No newline at end of file +kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ + +# Apply Horizontal Pod Autoscalers (HPA) +kubectl apply -n $CLUSTER_NAME -f hpa/ diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 22f4dc4..401114c 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50054 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "300m" + memory: "384Mi" env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index b9817eb..4632260 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50052 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "300m" + memory: "384Mi" env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index 2b5038e..d84f64f 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50051 + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index 32ee19f..00d86c7 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50057 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "300m" + memory: "384Mi" env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index 32b5aaf..3ab83a5 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50056 + resources: + requests: + cpu: "150m" + memory: "192Mi" + limits: + cpu: "400m" + memory: "448Mi" env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index 86800ac..ca4b053 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50053 + resources: + requests: + cpu: "150m" + memory: "192Mi" + limits: + cpu: "400m" + memory: "448Mi" env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index 8480665..d77fd82 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -18,6 +18,13 @@ spec: imagePullPolicy: Always ports: - containerPort: 50055 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "250m" + memory: "256Mi" env: - name: SERVICE_PORT valueFrom: From 2f4a8354abf3d80f4faedf9b5e3c7ee9fc979648 Mon Sep 17 00:00:00 2001 From: manuelfdg Date: Wed, 16 Apr 2025 01:58:10 +0000 Subject: [PATCH 02/18] ... --- code/kubernetes/scripts/cluster-info.sh | 26 +++++++++++++++++++++++++ code/kubernetes/scripts/deploy.sh | 1 + 2 files changed, 27 insertions(+) diff --git a/code/kubernetes/scripts/cluster-info.sh b/code/kubernetes/scripts/cluster-info.sh index 20723d5..6301967 100644 --- a/code/kubernetes/scripts/cluster-info.sh +++ b/code/kubernetes/scripts/cluster-info.sh @@ -11,6 +11,9 @@ SHOW_SERVICES=false SHOW_DEPLOYMENTS=false SHOW_RESOURCES_PODS=false SHOW_RESOURCES_NODES=false +SHOW_HPA=false +SHOW_DETAILED_RESOURCES=false +SHOW_EVENTS=false if [[ $# -eq 0 ]]; then SHOW_NAMESPACES=true @@ -19,6 +22,9 @@ if [[ $# -eq 0 ]]; then SHOW_DEPLOYMENTS=true SHOW_RESOURCES_PODS=true SHOW_RESOURCES_NODES=true + SHOW_HPA=true + SHOW_DETAILED_RESOURCES=true + SHOW_EVENTS=true fi # Parse flags @@ -30,6 +36,9 @@ while [[ "$#" -gt 0 ]]; do --deployments) SHOW_DEPLOYMENTS=true ;; --resources-pods) SHOW_RESOURCES_PODS=true ;; --resources-nodes) SHOW_RESOURCES_NODES=true ;; + --hpa) SHOW_HPA=true ;; + --detailed-resources) SHOW_DETAILED_RESOURCES=true ;; + --events) SHOW_EVENTS=true ;; --all) SHOW_NAMESPACES=true SHOW_PODS=true @@ -37,6 +46,9 @@ while [[ "$#" -gt 0 ]]; do SHOW_DEPLOYMENTS=true SHOW_RESOURCES_PODS=true SHOW_RESOURCES_NODES=true + SHOW_HPA=true + SHOW_DETAILED_RESOURCES=true + SHOW_EVENTS=true ;; *) echo "āŒ Unknown flag: $1"; exit 1 ;; esac @@ -53,3 +65,17 @@ $SHOW_SERVICES && echo -e "\nšŸ” Services:" && kubectl get svc -n $CLUSTER_NAME $SHOW_DEPLOYMENTS && echo -e "\nšŸ“‚ Deployments:" && kubectl get deployments -n $CLUSTER_NAME $SHOW_RESOURCES_PODS && echo -e "\nšŸ“Š Resource Usage (Pods):" && kubectl top pods -n $CLUSTER_NAME $SHOW_RESOURCES_NODES && echo -e "\nšŸ–„ļø Resource Usage (Nodes):" && kubectl top nodes + +# New sections for monitoring HPAs and resource limits +$SHOW_HPA && echo -e "\nāš–ļø Horizontal Pod Autoscalers:" && kubectl get hpa -n $CLUSTER_NAME + +if $SHOW_DETAILED_RESOURCES; then + echo -e "\nšŸ”Ž Detailed Resource Limits and Requests:" + echo "-------------------------------------------" + for pod in $(kubectl get pods -n $CLUSTER_NAME -o=name); do + echo -e "\nšŸ“Œ $pod" + kubectl describe $pod -n $CLUSTER_NAME | grep -A8 "Limits:" | grep -v "Node:" + done +fi + +$SHOW_EVENTS && echo -e "\nšŸ“œ Recent Events (including scaling):" && kubectl get events -n $CLUSTER_NAME --sort-by='.lastTimestamp' | grep -E '(HorizontalPodAutoscaler|scale|Scaled)' \ No newline at end of file diff --git a/code/kubernetes/scripts/deploy.sh b/code/kubernetes/scripts/deploy.sh index 2772453..f4ae4a2 100644 --- a/code/kubernetes/scripts/deploy.sh +++ b/code/kubernetes/scripts/deploy.sh @@ -46,6 +46,7 @@ gcloud container clusters get-credentials $CLUSTER_NAME --zone=$ZONE kubectl apply -n $CLUSTER_NAME -f config.yaml # Traefik +helm repo add traefik https://traefik.github.io/charts helm upgrade --install traefik traefik/traefik -n $CLUSTER_NAME -f traefik/values.yaml kubectl apply -n $CLUSTER_NAME -f traefik/cors.yaml kubectl apply -n $CLUSTER_NAME -f traefik/strip-prefix.yaml From 34a105dcfa51dbc6491c50ab8a5a5d7acd684158 Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Fri, 16 May 2025 12:20:11 +0100 Subject: [PATCH 03/18] Add Keycloak Authentication & Authorization --- code/docker-compose.yml | 21 ++ code/grpc-gateway/main.go | 103 +++++-- code/grpc-gateway/middleware/auth.go | 163 +++++++++++ code/grpc-gateway/middleware/auth_routes.go | 274 ++++++++++++++++++ code/keycloak/realm-export.json | 53 ++++ code/kubernetes/keycloak/configmap.yaml | 10 + code/kubernetes/keycloak/deployment.yaml | 63 ++++ code/kubernetes/keycloak/realm-configmap.yaml | 46 +++ code/kubernetes/keycloak/secrets.yaml | 11 + code/kubernetes/scripts/deploy.sh | 15 +- code/kubernetes/scripts/keycloak-ops.sh | 65 +++++ code/kubernetes/traefik/ingress-routes.yaml | 54 ++++ code/services/auth/auth.go | 120 ++++++++ code/traefik/traefik.yml | 51 ++++ 14 files changed, 1021 insertions(+), 28 deletions(-) create mode 100644 code/grpc-gateway/middleware/auth.go create mode 100644 code/grpc-gateway/middleware/auth_routes.go create mode 100644 code/keycloak/realm-export.json create mode 100644 code/kubernetes/keycloak/configmap.yaml create mode 100644 code/kubernetes/keycloak/deployment.yaml create mode 100644 code/kubernetes/keycloak/realm-configmap.yaml create mode 100644 code/kubernetes/keycloak/secrets.yaml create mode 100644 code/kubernetes/scripts/keycloak-ops.sh create mode 100644 code/kubernetes/traefik/ingress-routes.yaml create mode 100644 code/services/auth/auth.go diff --git a/code/docker-compose.yml b/code/docker-compose.yml index 261f24f..ba4b038 100644 --- a/code/docker-compose.yml +++ b/code/docker-compose.yml @@ -194,6 +194,27 @@ services: networks: - threadit-network + keycloak: + image: quay.io/keycloak/keycloak:21.1 + container_name: keycloak + restart: always + command: + - start-dev + - --import-realm + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_HOSTNAME_STRICT: false + KC_HOSTNAME_STRICT_HTTPS: false + KC_HTTP_ENABLED: "true" + KC_PROXY: edge + volumes: + - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro + ports: + - "${KEYCLOAK_PORT}:8080" + networks: + - threadit-network + volumes: db_data: driver: local diff --git a/code/grpc-gateway/main.go b/code/grpc-gateway/main.go index 71e7394..39017b2 100644 --- a/code/grpc-gateway/main.go +++ b/code/grpc-gateway/main.go @@ -16,6 +16,8 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + + "threadit/grpc-gateway/middleware" ) func getGrpcServerAddress(hostEnvVar string, portEnvVar string) string { @@ -31,8 +33,24 @@ func getGrpcServerAddress(hostEnvVar string, portEnvVar string) string { } func main() { - gwmux := runtime.NewServeMux() + ctx := context.Background() + mux := runtime.NewServeMux() + + // Initialize auth handler + authHandler := middleware.NewAuthHandler( + os.Getenv("KEYCLOAK_URL"), + os.Getenv("KEYCLOAK_CLIENT_ID"), + os.Getenv("KEYCLOAK_CLIENT_SECRET"), + os.Getenv("KEYCLOAK_REALM"), + ) + + // Create a new ServeMux for both gRPC-Gateway and auth routes + httpMux := http.NewServeMux() + + // Register auth routes + authHandler.RegisterRoutes(httpMux) + // gRPC dial options with message size configurations opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions( @@ -41,46 +59,77 @@ func main() { ), } - err := communitypb.RegisterCommunityServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT"), opts) - if err != nil { - log.Fatalf("Failed to register gRPC gateway: %v", err) + // Register gRPC-Gateway routes with auth middleware + httpMux.Handle("/api/v1/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Auth middleware for API routes + authMiddleware := middleware.NewAuthMiddleware(middleware.KeycloakConfig{ + Realm: os.Getenv("KEYCLOAK_REALM"), + ClientID: os.Getenv("KEYCLOAK_CLIENT_ID"), + ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"), + KeycloakURL: os.Getenv("KEYCLOAK_URL"), + }) + + authMiddleware.Handler(mux).ServeHTTP(w, r) + })) + + // Register service handlers + if err := registerServices(ctx, mux, opts); err != nil { + log.Fatalf("Failed to register services: %v", err) } - err = threadpb.RegisterThreadServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT"), opts) - if err != nil { - log.Fatalf("Failed to register gRPC gateway: %v", err) + port := os.Getenv("GRPC_GATEWAY_PORT") + if port == "" { + log.Fatalf("missing GRPC_GATEWAY_PORT env var") } - err = commentpb.RegisterCommentServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT"), opts) - if err != nil { - log.Fatalf("Failed to register gRPC gateway: %v", err) + log.Printf("gRPC Gateway server listening on :%s", port) + if err := http.ListenAndServe(":"+port, httpMux); err != nil { + log.Fatalf("Failed to serve: %v", err) } +} - err = votepb.RegisterVoteServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT"), opts) - if err != nil { - log.Fatalf("Failed to register gRPC gateway: %v", err) +func registerServices(ctx context.Context, mux *runtime.ServeMux, opts []grpc.DialOption) error { + // Register Community Service + if err := communitypb.RegisterCommunityServiceHandlerFromEndpoint( + ctx, mux, getGrpcServerAddress("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT"), opts, + ); err != nil { + return fmt.Errorf("failed to register community service: %v", err) } - err = searchpb.RegisterSearchServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT"), opts) - if err != nil { - log.Fatalf("Failed to register gRPC gateway: %v", err) + // Register Thread Service + if err := threadpb.RegisterThreadServiceHandlerFromEndpoint( + ctx, mux, getGrpcServerAddress("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT"), opts, + ); err != nil { + return fmt.Errorf("failed to register thread service: %v", err) } - err = popularpb.RegisterPopularServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT"), opts) - if err != nil { - log.Fatalf("Failed to register gRPC gateway: %v", err) + // Register Comment Service + if err := commentpb.RegisterCommentServiceHandlerFromEndpoint( + ctx, mux, getGrpcServerAddress("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT"), opts, + ); err != nil { + return fmt.Errorf("failed to register comment service: %v", err) } - http.Handle("/", gwmux) + // Register Vote Service + if err := votepb.RegisterVoteServiceHandlerFromEndpoint( + ctx, mux, getGrpcServerAddress("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT"), opts, + ); err != nil { + return fmt.Errorf("failed to register vote service: %v", err) + } - port := os.Getenv("GRPC_GATEWAY_PORT") - if port == "" { - log.Fatalf("missing GRPC_GATEWAY_PORT env var") + // Register Search Service + if err := searchpb.RegisterSearchServiceHandlerFromEndpoint( + ctx, mux, getGrpcServerAddress("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT"), opts, + ); err != nil { + return fmt.Errorf("failed to register search service: %v", err) } - log.Printf("gRPC Gateway server listening on :%s", port) - err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) - if err != nil { - log.Fatalf("Failed to start HTTP server: %v", err) + // Register Popular Service + if err := popularpb.RegisterPopularServiceHandlerFromEndpoint( + ctx, mux, getGrpcServerAddress("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT"), opts, + ); err != nil { + return fmt.Errorf("failed to register popular service: %v", err) } + + return nil } diff --git a/code/grpc-gateway/middleware/auth.go b/code/grpc-gateway/middleware/auth.go new file mode 100644 index 0000000..95607b1 --- /dev/null +++ b/code/grpc-gateway/middleware/auth.go @@ -0,0 +1,163 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc/metadata" + "your-module/code/services/auth" +) + +type AuthMiddleware struct { + keycloak *auth.KeycloakClient +} + +func NewAuthMiddleware(config auth.KeycloakConfig) (*AuthMiddleware, error) { + kc, err := auth.NewKeycloakClient(config) + if err != nil { + return nil, err + } + return &AuthMiddleware{keycloak: kc}, nil +} + +func (am *AuthMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Skip auth for public endpoints + if isPublicEndpoint(r.URL.Path, r.Method) { + next.ServeHTTP(w, r) + return + } + + // Extract token from Authorization header + token, err := auth.ExtractBearerToken(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Validate token + claims, err := am.keycloak.ValidateToken(r.Context(), token) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Check required roles for protected endpoints + if !hasRequiredRole(r.URL.Path, claims) { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + // Add user info to context + ctx := context.WithValue(r.Context(), "user_claims", claims) + + // Forward token to gRPC services + md := metadata.Pairs("authorization", "Bearer "+token) + ctx = metadata.NewOutgoingContext(ctx, md) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func isPublicEndpoint(path, method string) bool { + // Auth endpoints are always public + authPaths := []string{ + "/auth/login", + "/auth/register", + "/auth/logout", + } + for _, ap := range authPaths { + if path == ap { + return true + } + } + + // Only GET requests can be public for these paths + if method != http.MethodGet { + return false + } + + publicGetPaths := []string{ + "/communities", + "/threads", + "/comments", + "/search", + "/search/thread", + "/search/community", + "/popular/threads", + "/popular/comments", + } + + // Check exact matches for list endpoints + for _, pp := range publicGetPaths { + if path == pp { + return true + } + } + + // Check id based paths + idBasedPaths := []string{ + "/communities/", + "/threads/", + "/comments/", + } + + for _, pp := range idBasedPaths { + if strings.HasPrefix(path, pp) && path != pp { + return true + } + } + + return false +} + +func hasRequiredRole(path string, claims *auth.TokenClaims) bool { + roleRequirements := map[string]string{ + // Communities + "POST /communities": "user", + "PATCH /communities/": "moderator", + "DELETE /communities/": "moderator", + + // Threads + "POST /threads": "user", + "PATCH /threads/": "user", + "DELETE /threads/": "user", + + // Comment sdpoints + "POST /comments": "user", + "PATCH /comments/": "user", + "DELETE /comments/": "user", + + // Votes + "POST /votes/thread/": "user", + "POST /votes/comment/": "user", + + // Admin + "POST /admin/": "admin", + "PUT /admin/": "admin", + "DELETE /admin/": "admin", + } + + // Check each role requirement + for pathPattern, requiredRole := range roleRequirements { + parts := strings.SplitN(pathPattern, " ", 2) + method, pattern := parts[0], parts[1] + if strings.HasPrefix(path, pattern) { + return claims.RealmAccess.Roles != nil && contains(claims.RealmAccess.Roles, requiredRole) + } + } + + // If no specific role requirement, allow access + return true +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} \ No newline at end of file diff --git a/code/grpc-gateway/middleware/auth_routes.go b/code/grpc-gateway/middleware/auth_routes.go new file mode 100644 index 0000000..a81e597 --- /dev/null +++ b/code/grpc-gateway/middleware/auth_routes.go @@ -0,0 +1,274 @@ +package middleware + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +type AuthHandler struct { + keycloakURL string + clientID string + clientSecret string + realm string + adminClientID string +} + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type RegisterRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +type ErrorResponse struct { + Error string `json:"error"` + Description string `json:"error_description,omitempty"` +} + +func NewAuthHandler(keycloakURL, clientID, clientSecret, realm string) *AuthHandler { + return &AuthHandler{ + keycloakURL: keycloakURL, + clientID: clientID, + clientSecret: clientSecret, + realm: realm, + adminClientID: "admin-cli", + } +} + +func (h *AuthHandler) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/auth/register", h.handleRegister) + mux.HandleFunc("/auth/login", h.handleLogin) + mux.HandleFunc("/auth/logout", h.handleLogout) +} + +func (h *AuthHandler) validateRegistration(req *RegisterRequest) error { + // Username validation + if len(req.Username) < 3 || len(req.Username) > 30 { + return fmt.Errorf("username must be between 3 and 30 characters") + } + if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(req.Username) { + return fmt.Errorf("username can only contain letters, numbers, underscores, and hyphens") + } + + // Email validation + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + if !emailRegex.MatchString(req.Email) { + return fmt.Errorf("invalid email format") + } + + // Password validation + if len(req.Password) < 8 { + return fmt.Errorf("password must be at least 8 characters long") + } + if !regexp.MustCompile(`[A-Z]`).MatchString(req.Password) { + return fmt.Errorf("password must contain at least one uppercase letter") + } + if !regexp.MustCompile(`[a-z]`).MatchString(req.Password) { + return fmt.Errorf("password must contain at least one lowercase letter") + } + if !regexp.MustCompile(`[0-9]`).MatchString(req.Password) { + return fmt.Errorf("password must contain at least one number") + } + + return nil +} + +func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req RegisterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.sendError(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Validate registration data + if err := h.validateRegistration(&req); err != nil { + h.sendError(w, err.Error(), http.StatusBadRequest) + return + } + + // Create user in Keycloak + keycloakURL := fmt.Sprintf("%s/auth/admin/realms/%s/users", h.keycloakURL, h.realm) + userData := map[string]interface{}{ + "username": req.Username, + "email": req.Email, + "enabled": true, + "credentials": []map[string]interface{}{ + { + "type": "password", + "value": req.Password, + "temporary": false, + }, + }, + } + + jsonData, err := json.Marshal(userData) + if err != nil { + h.sendError(w, "Internal server error", http.StatusInternalServerError) + return + } + + // Get admin token + adminToken, err := h.getAdminToken() + if err != nil { + h.sendError(w, "Failed to authenticate with Keycloak", http.StatusInternalServerError) + return + } + + request, err := http.NewRequest(http.MethodPost, keycloakURL, strings.NewReader(string(jsonData))) + if err != nil { + h.sendError(w, "Internal server error", http.StatusInternalServerError) + return + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", "Bearer "+adminToken) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(request) + if err != nil { + h.sendError(w, "Failed to register user", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + h.sendError(w, fmt.Sprintf("Failed to register user: %s", string(body)), resp.StatusCode) + return + } + + // Auto login after registration + h.performLogin(w, req.Username, req.Password) +} + +func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.sendError(w, "Invalid request body", http.StatusBadRequest) + return + } + + h.performLogin(w, req.Username, req.Password) +} + +func (h *AuthHandler) performLogin(w http.ResponseWriter, username, password string) { + tokenURL := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", h.keycloakURL, h.realm) + data := url.Values{} + data.Set("grant_type", "password") + data.Set("client_id", h.clientID) + data.Set("client_secret", h.clientSecret) + data.Set("username", username) + data.Set("password", password) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.PostForm(tokenURL, data) + if err != nil { + h.sendError(w, "Failed to login", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + h.sendError(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + // Forward Keycloak response (tokens) to client + w.Header().Set("Content-Type", "application/json") + io.Copy(w, resp.Body) +} + +func (h *AuthHandler) handleLogout(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + token := r.Header.Get("Authorization") + if token == "" { + h.sendError(w, "No token provided", http.StatusBadRequest) + return + } + + logoutURL := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/logout", h.keycloakURL, h.realm) + request, err := http.NewRequest(http.MethodPost, logoutURL, nil) + if err != nil { + h.sendError(w, "Internal server error", http.StatusInternalServerError) + return + } + + request.Header.Set("Authorization", token) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(request) + if err != nil { + h.sendError(w, "Failed to logout", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + h.sendError(w, "Failed to logout", resp.StatusCode) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Logged out successfully"}) +} + +func (h *AuthHandler) getAdminToken() (string, error) { + tokenURL := fmt.Sprintf("%s/auth/realms/master/protocol/openid-connect/token", h.keycloakURL) + data := url.Values{} + data.Set("grant_type", "client_credentials") + data.Set("client_id", h.adminClientID) + data.Set("client_secret", h.clientSecret) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.PostForm(tokenURL, data) + if err != nil { + return "", fmt.Errorf("failed to get admin token: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to get admin token: status %d", resp.StatusCode) + } + + var result struct { + AccessToken string `json:"access_token"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("failed to decode admin token response: %v", err) + } + + return result.AccessToken, nil +} + +func (h *AuthHandler) sendError(w http.ResponseWriter, message string, status int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(ErrorResponse{ + Error: http.StatusText(status), + Description: message, + }) +} \ No newline at end of file diff --git a/code/keycloak/realm-export.json b/code/keycloak/realm-export.json new file mode 100644 index 0000000..9452d7e --- /dev/null +++ b/code/keycloak/realm-export.json @@ -0,0 +1,53 @@ +{ + "id": "threadit", + "realm": "threadit", + "enabled": true, + "roles": { + "realm": [ + { + "name": "user", + "description": "User role" + }, + { + "name": "moderator", + "description": "Community moderator role" + }, + { + "name": "admin", + "description": "Admin role" + } + ] + }, + "defaultRoles": ["user"], + "clients": [ + { + "clientId": "threadit-api", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "clientAuthenticatorType": "client-secret", + "secret": "${CLIENT_SECRET}", + "redirectUris": ["*"], + "webOrigins": ["*"], + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true + } + ], + "users": [ + { + "username": "admin", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "${ADMIN_PASSWORD}", + "temporary": false + } + ], + "realmRoles": ["admin"] + } + ] +} \ No newline at end of file diff --git a/code/kubernetes/keycloak/configmap.yaml b/code/kubernetes/keycloak/configmap.yaml new file mode 100644 index 0000000..ea226ad --- /dev/null +++ b/code/kubernetes/keycloak/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-config + namespace: threadit +data: + KC_HOSTNAME_STRICT: "false" + KC_HOSTNAME_STRICT_HTTPS: "false" + KC_HTTP_ENABLED: "true" + KC_PROXY: "edge" \ No newline at end of file diff --git a/code/kubernetes/keycloak/deployment.yaml b/code/kubernetes/keycloak/deployment.yaml new file mode 100644 index 0000000..d2b08bc --- /dev/null +++ b/code/kubernetes/keycloak/deployment.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: Service +metadata: + name: keycloak + namespace: threadit + labels: + app: keycloak +spec: + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + selector: + app: keycloak +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + namespace: threadit + labels: + app: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:21.1 + args: ["start-dev", "--import-realm"] + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: keycloak-config + - secretRef: + name: keycloak-secrets + volumeMounts: + - name: realm-config + mountPath: /opt/keycloak/data/import + readOnly: true + readinessProbe: + httpGet: + path: /auth/realms/master + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /auth/realms/master + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 15 + volumes: + - name: realm-config + configMap: + name: keycloak-realm-config \ No newline at end of file diff --git a/code/kubernetes/keycloak/realm-configmap.yaml b/code/kubernetes/keycloak/realm-configmap.yaml new file mode 100644 index 0000000..5a1a905 --- /dev/null +++ b/code/kubernetes/keycloak/realm-configmap.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-realm-config + namespace: threadit +data: + realm.json: | + { + "id": "threadit", + "realm": "threadit", + "enabled": true, + "roles": { + "realm": [ + { + "name": "user", + "description": "User role" + }, + { + "name": "moderator", + "description": "Community moderator role" + }, + { + "name": "admin", + "description": "Admin role" + } + ] + }, + "defaultRoles": ["user"], + "clients": [ + { + "clientId": "threadit-api", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "clientAuthenticatorType": "client-secret", + "secret": "${CLIENT_SECRET}", + "redirectUris": ["*"], + "webOrigins": ["*"], + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true + } + ] + } \ No newline at end of file diff --git a/code/kubernetes/keycloak/secrets.yaml b/code/kubernetes/keycloak/secrets.yaml new file mode 100644 index 0000000..fc4baa5 --- /dev/null +++ b/code/kubernetes/keycloak/secrets.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-secrets + namespace: threadit +type: Opaque +data: + KC_DB_PASSWORD: a2V5Y2xvYWtfcGFzc3dvcmQ= + KEYCLOAK_ADMIN: YWRtaW4= + KEYCLOAK_ADMIN_PASSWORD: YWRtaW5fcGFzc3dvcmQ= + CLIENT_SECRET: eW91ci1jbGllbnQtc2VjcmV0 \ No newline at end of file diff --git a/code/kubernetes/scripts/deploy.sh b/code/kubernetes/scripts/deploy.sh index d8117c1..7016757 100644 --- a/code/kubernetes/scripts/deploy.sh +++ b/code/kubernetes/scripts/deploy.sh @@ -49,14 +49,27 @@ kubectl apply -n $CLUSTER_NAME -f config.yaml helm upgrade --install traefik traefik/traefik -n $CLUSTER_NAME -f traefik/values.yaml kubectl apply -n $CLUSTER_NAME -f traefik/cors.yaml kubectl apply -n $CLUSTER_NAME -f traefik/strip-prefix.yaml +kubectl apply -n $CLUSTER_NAME -f traefik/ingress-routes.yaml # MongoDB kubectl apply -n $CLUSTER_NAME -f mongo/ +# Keycloak +echo "Deploying Keycloak..." +kubectl apply -n $CLUSTER_NAME -f keycloak/configmap.yaml +kubectl apply -n $CLUSTER_NAME -f keycloak/secrets.yaml +kubectl apply -n $CLUSTER_NAME -f keycloak/realm-configmap.yaml +kubectl apply -n $CLUSTER_NAME -f keycloak/deployment.yaml + # Services for SERVICE in "${SERVICES[@]}"; do kubectl apply -n $CLUSTER_NAME -f services/$SERVICE/ done # gRPC Gateway -kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ \ No newline at end of file +kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ + +echo "Waiting for Keycloak to be ready..." +kubectl wait --for=condition=ready pod -l app=keycloak -n $CLUSTER_NAME --timeout=300s + +echo "Deployment complete!" \ No newline at end of file diff --git a/code/kubernetes/scripts/keycloak-ops.sh b/code/kubernetes/scripts/keycloak-ops.sh new file mode 100644 index 0000000..170a5f4 --- /dev/null +++ b/code/kubernetes/scripts/keycloak-ops.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +CLUSTER_NAME="threadit-cluster" + +function help() { + echo "Usage: $0 " + echo "Commands:" + echo " status - Check Keycloak status" + echo " logs - Show Keycloak logs" + echo " restart - Restart Keycloak deployment" + echo " reload - Reload realm configuration" + echo " port-forward - Start port forwarding to access Keycloak locally" +} + +function check_status() { + echo "Checking Keycloak status..." + kubectl get pods -n $CLUSTER_NAME -l app=keycloak +} + +function show_logs() { + echo "Fetching Keycloak logs..." + kubectl logs -n $CLUSTER_NAME -l app=keycloak --tail=100 -f +} + +function restart_keycloak() { + echo "Restarting Keycloak..." + kubectl rollout restart deployment/keycloak -n $CLUSTER_NAME + kubectl rollout status deployment/keycloak -n $CLUSTER_NAME +} + +function reload_realm() { + echo "Reloading realm configuration..." + # Delete the existing pod to force a reload of the realm config + kubectl delete pod -n $CLUSTER_NAME -l app=keycloak + echo "Waiting for new pod to be ready..." + kubectl wait --for=condition=ready pod -l app=keycloak -n $CLUSTER_NAME --timeout=300s +} + +function port_forward() { + echo "Starting port forward to Keycloak on localhost:8080..." + kubectl port-forward -n $CLUSTER_NAME svc/keycloak 8080:8080 +} + +case "$1" in + "status") + check_status + ;; + "logs") + show_logs + ;; + "restart") + restart_keycloak + ;; + "reload") + reload_realm + ;; + "port-forward") + port_forward + ;; + *) + help + exit 1 + ;; +esac \ No newline at end of file diff --git a/code/kubernetes/traefik/ingress-routes.yaml b/code/kubernetes/traefik/ingress-routes.yaml new file mode 100644 index 0000000..5620952 --- /dev/null +++ b/code/kubernetes/traefik/ingress-routes.yaml @@ -0,0 +1,54 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: cors-headers + namespace: threadit +spec: + headers: + accessControlAllowMethods: + - GET + - POST + - PUT + - DELETE + - PATCH + accessControlAllowHeaders: + - "Authorization" + - "Content-Type" + accessControlAllowOriginList: + - "*" + accessControlMaxAge: 100 + addVaryHeader: true +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: api + namespace: threadit +spec: + entryPoints: + - web + routes: + - match: PathPrefix(`/api/v1`) + kind: Rule + services: + - name: grpc-gateway + port: 8080 + middlewares: + - name: cors-headers +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: keycloak + namespace: threadit +spec: + entryPoints: + - web + routes: + - match: PathPrefix(`/auth`) + kind: Rule + services: + - name: keycloak + port: 8080 + middlewares: + - name: cors-headers \ No newline at end of file diff --git a/code/services/auth/auth.go b/code/services/auth/auth.go new file mode 100644 index 0000000..faab083 --- /dev/null +++ b/code/services/auth/auth.go @@ -0,0 +1,120 @@ +package auth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + "github.com/golang-jwt/jwt/v4" +) + +var ( + ErrNoToken = errors.New("no token provided") + ErrInvalidToken = errors.New("invalid token") + ErrInsufficientRole = errors.New("insufficient role") +) + +type KeycloakConfig struct { + Realm string + ClientID string + ClientSecret string + KeycloakURL string +} + +type TokenClaims struct { + jwt.StandardClaims + RealmAccess struct { + Roles []string `json:"roles"` + } `json:"realm_access"` +} + +type KeycloakClient struct { + config KeycloakConfig + keys map[string]interface{} +} + +func NewKeycloakClient(config KeycloakConfig) (*KeycloakClient, error) { + kc := &KeycloakClient{ + config: config, + keys: make(map[string]interface{}), + } + if err := kc.fetchKeys(); err != nil { + return nil, err + } + return kc, nil +} + +func (kc *KeycloakClient) fetchKeys() error { + resp, err := http.Get(fmt.Sprintf("%s/realms/%s/protocol/openid-connect/certs", kc.config.KeycloakURL, kc.config.Realm)) + if err != nil { + return err + } + defer resp.Body.Close() + + var jwks struct { + Keys []struct { + Kid string `json:"kid"` + Kty string `json:"kty"` + Alg string `json:"alg"` + Use string `json:"use"` + N string `json:"n"` + E string `json:"e"` + } `json:"keys"` + } + + if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { + return err + } + + for _, key := range jwks.Keys { + kc.keys[key.Kid] = key + } + + return nil +} + +func (kc *KeycloakClient) ValidateToken(ctx context.Context, tokenString string) (*TokenClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { + if kid, ok := token.Header["kid"].(string); ok { + if key, exists := kc.keys[kid]; exists { + return key, nil + } + } + return nil, ErrInvalidToken + }) + + if err != nil { + return nil, fmt.Errorf("failed to parse token: %w", err) + } + + if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid { + return claims, nil + } + + return nil, ErrInvalidToken +} + +func (kc *KeycloakClient) HasRole(claims *TokenClaims, requiredRole string) bool { + for _, role := range claims.RealmAccess.Roles { + if role == requiredRole { + return true + } + } + return false +} + +func ExtractBearerToken(header string) (string, error) { + if header == "" { + return "", ErrNoToken + } + + parts := strings.Split(header, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return "", ErrInvalidToken + } + + return parts[1], nil +} \ No newline at end of file diff --git a/code/traefik/traefik.yml b/code/traefik/traefik.yml index 58366a4..3ac99dd 100644 --- a/code/traefik/traefik.yml +++ b/code/traefik/traefik.yml @@ -3,12 +3,63 @@ global: sendAnonymousUsage: false api: + dashboard: true insecure: true entryPoints: web: address: ":80" + forwardedHeaders: + insecure: true providers: + docker: + exposedByDefault: false file: filename: "/etc/traefik/dynamic.yml" + +http: + middlewares: + cors-headers: + headers: + accessControlAllowMethods: + - GET + - POST + - PUT + - DELETE + - PATCH + accessControlAllowHeaders: + - "Authorization" + - "Content-Type" + accessControlAllowOriginList: + - "*" + accessControlMaxAge: 100 + addVaryHeader: true + + routers: + api: + rule: "PathPrefix(`/api/v1`)" + service: "grpc-gateway" + middlewares: + - "cors-headers" + entryPoints: + - "web" + + keycloak: + rule: "PathPrefix(`/auth`)" + service: "keycloak" + middlewares: + - "cors-headers" + entryPoints: + - "web" + + services: + grpc-gateway: + loadBalancer: + servers: + - url: "http://grpc-gateway:8080" + + keycloak: + loadBalancer: + servers: + - url: "http://keycloak:8080" From aafed7c7323826537d927be18da6be7171239b80 Mon Sep 17 00:00:00 2001 From: cacobaco Date: Sun, 18 May 2025 11:53:12 +0100 Subject: [PATCH 04/18] Add liveness and readiness probes --- .../comment-service/pb/comment-service.pb.go | 33 ++++---- .../pb/comment-service_grpc.pb.go | 40 +++++++++- .../pb/community-service.pb.go | 33 ++++---- .../pb/community-service_grpc.pb.go | 40 +++++++++- code/gen/db-service/pb/db-service.pb.go | 2 +- code/gen/db-service/pb/db-service_grpc.pb.go | 2 +- code/gen/models/pb/models.pb.go | 2 +- .../popular-service/pb/popular-service.pb.go | 25 +++--- .../pb/popular-service_grpc.pb.go | 41 +++++++++- .../search-service/pb/search-service.pb.go | 29 ++++--- .../pb/search-service_grpc.pb.go | 41 +++++++++- .../thread-service/pb/thread-service.pb.go | 33 ++++---- .../pb/thread-service_grpc.pb.go | 40 +++++++++- code/gen/vote-service/pb/vote-service.pb.go | 29 ++++--- .../vote-service/pb/vote-service_grpc.pb.go | 40 +++++++++- code/grpc-gateway/main.go | 79 +++++++++++++++++++ code/kubernetes/grpc-gateway/deployment.yaml | 12 ++- code/kubernetes/mongo/deployment.yaml | 16 ++++ .../services/comment-service/deployment.yaml | 10 ++- .../community-service/deployment.yaml | 10 ++- .../services/db-service/deployment.yaml | 8 ++ .../services/popular-service/deployment.yaml | 10 ++- .../services/search-service/deployment.yaml | 10 ++- .../services/thread-service/deployment.yaml | 10 ++- .../services/vote-service/deployment.yaml | 10 ++- code/proto/comment-service.proto | 2 + code/proto/community-service.proto | 2 + code/proto/popular-service.proto | 4 + code/proto/search-service.proto | 3 + code/proto/thread-service.proto | 2 + code/proto/vote-service.proto | 2 + code/services/comment-service/src/server.go | 4 + code/services/community-service/src/server.go | 7 +- code/services/popular-service/src/server.go | 5 ++ code/services/search-service/src/server.go | 6 ++ code/services/thread-service/src/server.go | 4 + code/services/vote-service/src/server.go | 5 ++ code/traefik/dynamic.yml | 13 +++ 38 files changed, 567 insertions(+), 97 deletions(-) diff --git a/code/gen/comment-service/pb/comment-service.pb.go b/code/gen/comment-service/pb/comment-service.pb.go index 7732cdc..7664ecb 100644 --- a/code/gen/comment-service/pb/comment-service.pb.go +++ b/code/gen/comment-service/pb/comment-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: comment-service.proto package pb @@ -434,8 +434,9 @@ const file_comment_service_proto_rawDesc = "" + "\f_vote_offsetB\x16\n" + "\x14_num_comments_offset\"&\n" + "\x14DeleteCommentRequest\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id2\xec\x03\n" + - "\x0eCommentService\x12^\n" + + "\x02id\x18\x01 \x01(\tR\x02id2\xab\x04\n" + + "\x0eCommentService\x12=\n" + + "\vCheckHealth\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12^\n" + "\fListComments\x12\x1c.comment.ListCommentsRequest\x1a\x1d.comment.ListCommentsResponse\"\x11\x82\xd3\xe4\x93\x02\v\x12\t/comments\x12d\n" + "\rCreateComment\x12\x1d.comment.CreateCommentRequest\x1a\x1e.comment.CreateCommentResponse\"\x14\x82\xd3\xe4\x93\x02\x0e:\x01*\"\t/comments\x12Q\n" + "\n" + @@ -471,18 +472,20 @@ var file_comment_service_proto_goTypes = []any{ var file_comment_service_proto_depIdxs = []int32{ 7, // 0: comment.ListCommentsResponse.comments:type_name -> models.Comment 8, // 1: comment.CreateCommentRequest.parent_type:type_name -> models.CommentParentType - 0, // 2: comment.CommentService.ListComments:input_type -> comment.ListCommentsRequest - 2, // 3: comment.CommentService.CreateComment:input_type -> comment.CreateCommentRequest - 4, // 4: comment.CommentService.GetComment:input_type -> comment.GetCommentRequest - 5, // 5: comment.CommentService.UpdateComment:input_type -> comment.UpdateCommentRequest - 6, // 6: comment.CommentService.DeleteComment:input_type -> comment.DeleteCommentRequest - 1, // 7: comment.CommentService.ListComments:output_type -> comment.ListCommentsResponse - 3, // 8: comment.CommentService.CreateComment:output_type -> comment.CreateCommentResponse - 7, // 9: comment.CommentService.GetComment:output_type -> models.Comment - 9, // 10: comment.CommentService.UpdateComment:output_type -> google.protobuf.Empty - 9, // 11: comment.CommentService.DeleteComment:output_type -> google.protobuf.Empty - 7, // [7:12] is the sub-list for method output_type - 2, // [2:7] is the sub-list for method input_type + 9, // 2: comment.CommentService.CheckHealth:input_type -> google.protobuf.Empty + 0, // 3: comment.CommentService.ListComments:input_type -> comment.ListCommentsRequest + 2, // 4: comment.CommentService.CreateComment:input_type -> comment.CreateCommentRequest + 4, // 5: comment.CommentService.GetComment:input_type -> comment.GetCommentRequest + 5, // 6: comment.CommentService.UpdateComment:input_type -> comment.UpdateCommentRequest + 6, // 7: comment.CommentService.DeleteComment:input_type -> comment.DeleteCommentRequest + 9, // 8: comment.CommentService.CheckHealth:output_type -> google.protobuf.Empty + 1, // 9: comment.CommentService.ListComments:output_type -> comment.ListCommentsResponse + 3, // 10: comment.CommentService.CreateComment:output_type -> comment.CreateCommentResponse + 7, // 11: comment.CommentService.GetComment:output_type -> models.Comment + 9, // 12: comment.CommentService.UpdateComment:output_type -> google.protobuf.Empty + 9, // 13: comment.CommentService.DeleteComment:output_type -> google.protobuf.Empty + 8, // [8:14] is the sub-list for method output_type + 2, // [2:8] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name diff --git a/code/gen/comment-service/pb/comment-service_grpc.pb.go b/code/gen/comment-service/pb/comment-service_grpc.pb.go index 5ce5698..5923000 100644 --- a/code/gen/comment-service/pb/comment-service_grpc.pb.go +++ b/code/gen/comment-service/pb/comment-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: comment-service.proto package pb @@ -21,6 +21,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + CommentService_CheckHealth_FullMethodName = "/comment.CommentService/CheckHealth" CommentService_ListComments_FullMethodName = "/comment.CommentService/ListComments" CommentService_CreateComment_FullMethodName = "/comment.CommentService/CreateComment" CommentService_GetComment_FullMethodName = "/comment.CommentService/GetComment" @@ -32,6 +33,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type CommentServiceClient interface { + CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) ListComments(ctx context.Context, in *ListCommentsRequest, opts ...grpc.CallOption) (*ListCommentsResponse, error) CreateComment(ctx context.Context, in *CreateCommentRequest, opts ...grpc.CallOption) (*CreateCommentResponse, error) GetComment(ctx context.Context, in *GetCommentRequest, opts ...grpc.CallOption) (*pb.Comment, error) @@ -47,6 +49,16 @@ func NewCommentServiceClient(cc grpc.ClientConnInterface) CommentServiceClient { return &commentServiceClient{cc} } +func (c *commentServiceClient) CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, CommentService_CheckHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *commentServiceClient) ListComments(ctx context.Context, in *ListCommentsRequest, opts ...grpc.CallOption) (*ListCommentsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListCommentsResponse) @@ -101,6 +113,7 @@ func (c *commentServiceClient) DeleteComment(ctx context.Context, in *DeleteComm // All implementations must embed UnimplementedCommentServiceServer // for forward compatibility. type CommentServiceServer interface { + CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) ListComments(context.Context, *ListCommentsRequest) (*ListCommentsResponse, error) CreateComment(context.Context, *CreateCommentRequest) (*CreateCommentResponse, error) GetComment(context.Context, *GetCommentRequest) (*pb.Comment, error) @@ -116,6 +129,9 @@ type CommentServiceServer interface { // pointer dereference when methods are called. type UnimplementedCommentServiceServer struct{} +func (UnimplementedCommentServiceServer) CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented") +} func (UnimplementedCommentServiceServer) ListComments(context.Context, *ListCommentsRequest) (*ListCommentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListComments not implemented") } @@ -152,6 +168,24 @@ func RegisterCommentServiceServer(s grpc.ServiceRegistrar, srv CommentServiceSer s.RegisterService(&CommentService_ServiceDesc, srv) } +func _CommentService_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CommentServiceServer).CheckHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CommentService_CheckHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CommentServiceServer).CheckHealth(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _CommentService_ListComments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListCommentsRequest) if err := dec(in); err != nil { @@ -249,6 +283,10 @@ var CommentService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "comment.CommentService", HandlerType: (*CommentServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "CheckHealth", + Handler: _CommentService_CheckHealth_Handler, + }, { MethodName: "ListComments", Handler: _CommentService_ListComments_Handler, diff --git a/code/gen/community-service/pb/community-service.pb.go b/code/gen/community-service/pb/community-service.pb.go index d386cb6..8de473a 100644 --- a/code/gen/community-service/pb/community-service.pb.go +++ b/code/gen/community-service/pb/community-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: community-service.proto package pb @@ -391,8 +391,9 @@ const file_community_service_proto_rawDesc = "" + "\x05_nameB\x15\n" + "\x13_num_threads_offset\"(\n" + "\x16DeleteCommunityRequest\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id2\xa8\x04\n" + - "\x10CommunityService\x12n\n" + + "\x02id\x18\x01 \x01(\tR\x02id2\xe7\x04\n" + + "\x10CommunityService\x12=\n" + + "\vCheckHealth\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12n\n" + "\x0fListCommunities\x12!.community.ListCommunitiesRequest\x1a\".community.ListCommunitiesResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/communities\x12q\n" + "\x0fCreateCommunity\x12!.community.CreateCommunityRequest\x1a\".community.CreateCommunityResponse\"\x17\x82\xd3\xe4\x93\x02\x11:\x01*\"\f/communities\x12\\\n" + "\fGetCommunity\x12\x1e.community.GetCommunityRequest\x1a\x11.models.Community\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/communities/{id}\x12j\n" + @@ -425,18 +426,20 @@ var file_community_service_proto_goTypes = []any{ } var file_community_service_proto_depIdxs = []int32{ 7, // 0: community.ListCommunitiesResponse.communities:type_name -> models.Community - 0, // 1: community.CommunityService.ListCommunities:input_type -> community.ListCommunitiesRequest - 2, // 2: community.CommunityService.CreateCommunity:input_type -> community.CreateCommunityRequest - 4, // 3: community.CommunityService.GetCommunity:input_type -> community.GetCommunityRequest - 5, // 4: community.CommunityService.UpdateCommunity:input_type -> community.UpdateCommunityRequest - 6, // 5: community.CommunityService.DeleteCommunity:input_type -> community.DeleteCommunityRequest - 1, // 6: community.CommunityService.ListCommunities:output_type -> community.ListCommunitiesResponse - 3, // 7: community.CommunityService.CreateCommunity:output_type -> community.CreateCommunityResponse - 7, // 8: community.CommunityService.GetCommunity:output_type -> models.Community - 8, // 9: community.CommunityService.UpdateCommunity:output_type -> google.protobuf.Empty - 8, // 10: community.CommunityService.DeleteCommunity:output_type -> google.protobuf.Empty - 6, // [6:11] is the sub-list for method output_type - 1, // [1:6] is the sub-list for method input_type + 8, // 1: community.CommunityService.CheckHealth:input_type -> google.protobuf.Empty + 0, // 2: community.CommunityService.ListCommunities:input_type -> community.ListCommunitiesRequest + 2, // 3: community.CommunityService.CreateCommunity:input_type -> community.CreateCommunityRequest + 4, // 4: community.CommunityService.GetCommunity:input_type -> community.GetCommunityRequest + 5, // 5: community.CommunityService.UpdateCommunity:input_type -> community.UpdateCommunityRequest + 6, // 6: community.CommunityService.DeleteCommunity:input_type -> community.DeleteCommunityRequest + 8, // 7: community.CommunityService.CheckHealth:output_type -> google.protobuf.Empty + 1, // 8: community.CommunityService.ListCommunities:output_type -> community.ListCommunitiesResponse + 3, // 9: community.CommunityService.CreateCommunity:output_type -> community.CreateCommunityResponse + 7, // 10: community.CommunityService.GetCommunity:output_type -> models.Community + 8, // 11: community.CommunityService.UpdateCommunity:output_type -> google.protobuf.Empty + 8, // 12: community.CommunityService.DeleteCommunity:output_type -> google.protobuf.Empty + 7, // [7:13] is the sub-list for method output_type + 1, // [1:7] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name diff --git a/code/gen/community-service/pb/community-service_grpc.pb.go b/code/gen/community-service/pb/community-service_grpc.pb.go index f75f528..a36beaf 100644 --- a/code/gen/community-service/pb/community-service_grpc.pb.go +++ b/code/gen/community-service/pb/community-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: community-service.proto package pb @@ -21,6 +21,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + CommunityService_CheckHealth_FullMethodName = "/community.CommunityService/CheckHealth" CommunityService_ListCommunities_FullMethodName = "/community.CommunityService/ListCommunities" CommunityService_CreateCommunity_FullMethodName = "/community.CommunityService/CreateCommunity" CommunityService_GetCommunity_FullMethodName = "/community.CommunityService/GetCommunity" @@ -32,6 +33,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type CommunityServiceClient interface { + CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) ListCommunities(ctx context.Context, in *ListCommunitiesRequest, opts ...grpc.CallOption) (*ListCommunitiesResponse, error) CreateCommunity(ctx context.Context, in *CreateCommunityRequest, opts ...grpc.CallOption) (*CreateCommunityResponse, error) GetCommunity(ctx context.Context, in *GetCommunityRequest, opts ...grpc.CallOption) (*pb.Community, error) @@ -47,6 +49,16 @@ func NewCommunityServiceClient(cc grpc.ClientConnInterface) CommunityServiceClie return &communityServiceClient{cc} } +func (c *communityServiceClient) CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, CommunityService_CheckHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *communityServiceClient) ListCommunities(ctx context.Context, in *ListCommunitiesRequest, opts ...grpc.CallOption) (*ListCommunitiesResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListCommunitiesResponse) @@ -101,6 +113,7 @@ func (c *communityServiceClient) DeleteCommunity(ctx context.Context, in *Delete // All implementations must embed UnimplementedCommunityServiceServer // for forward compatibility. type CommunityServiceServer interface { + CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) ListCommunities(context.Context, *ListCommunitiesRequest) (*ListCommunitiesResponse, error) CreateCommunity(context.Context, *CreateCommunityRequest) (*CreateCommunityResponse, error) GetCommunity(context.Context, *GetCommunityRequest) (*pb.Community, error) @@ -116,6 +129,9 @@ type CommunityServiceServer interface { // pointer dereference when methods are called. type UnimplementedCommunityServiceServer struct{} +func (UnimplementedCommunityServiceServer) CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented") +} func (UnimplementedCommunityServiceServer) ListCommunities(context.Context, *ListCommunitiesRequest) (*ListCommunitiesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListCommunities not implemented") } @@ -152,6 +168,24 @@ func RegisterCommunityServiceServer(s grpc.ServiceRegistrar, srv CommunityServic s.RegisterService(&CommunityService_ServiceDesc, srv) } +func _CommunityService_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CommunityServiceServer).CheckHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CommunityService_CheckHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CommunityServiceServer).CheckHealth(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _CommunityService_ListCommunities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListCommunitiesRequest) if err := dec(in); err != nil { @@ -249,6 +283,10 @@ var CommunityService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "community.CommunityService", HandlerType: (*CommunityServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "CheckHealth", + Handler: _CommunityService_CheckHealth_Handler, + }, { MethodName: "ListCommunities", Handler: _CommunityService_ListCommunities_Handler, diff --git a/code/gen/db-service/pb/db-service.pb.go b/code/gen/db-service/pb/db-service.pb.go index b6e4724..a54a036 100644 --- a/code/gen/db-service/pb/db-service.pb.go +++ b/code/gen/db-service/pb/db-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: db-service.proto package pb diff --git a/code/gen/db-service/pb/db-service_grpc.pb.go b/code/gen/db-service/pb/db-service_grpc.pb.go index 854c780..8f97d76 100644 --- a/code/gen/db-service/pb/db-service_grpc.pb.go +++ b/code/gen/db-service/pb/db-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: db-service.proto package pb diff --git a/code/gen/models/pb/models.pb.go b/code/gen/models/pb/models.pb.go index 8bf0cf9..9659e27 100644 --- a/code/gen/models/pb/models.pb.go +++ b/code/gen/models/pb/models.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: models.proto package models diff --git a/code/gen/popular-service/pb/popular-service.pb.go b/code/gen/popular-service/pb/popular-service.pb.go index afa8b4e..0cd3dcb 100644 --- a/code/gen/popular-service/pb/popular-service.pb.go +++ b/code/gen/popular-service/pb/popular-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: popular-service.proto package pb @@ -11,6 +11,7 @@ import ( _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -219,7 +220,7 @@ var File_popular_service_proto protoreflect.FileDescriptor const file_popular_service_proto_rawDesc = "" + "\n" + - "\x15popular-service.proto\x12\apopular\x1a\x1cgoogle/api/annotations.proto\x1a\fmodels.proto\"g\n" + + "\x15popular-service.proto\x12\apopular\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/api/annotations.proto\x1a\fmodels.proto\"g\n" + "\x18GetPopularThreadsRequest\x12\x1b\n" + "\x06offset\x18\x01 \x01(\x05H\x00R\x06offset\x88\x01\x01\x12\x19\n" + "\x05limit\x18\x02 \x01(\x05H\x01R\x05limit\x88\x01\x01B\t\n" + @@ -233,8 +234,9 @@ const file_popular_service_proto_rawDesc = "" + "\a_offsetB\b\n" + "\x06_limit\"I\n" + "\x1aGetPopularCommentsResponse\x12+\n" + - "\bcomments\x18\x01 \x03(\v2\x0f.models.CommentR\bcomments2\x80\x02\n" + - "\x0ePopularService\x12t\n" + + "\bcomments\x18\x01 \x03(\v2\x0f.models.CommentR\bcomments2\xbf\x02\n" + + "\x0ePopularService\x12=\n" + + "\vCheckHealth\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12t\n" + "\x11GetPopularThreads\x12!.popular.GetPopularThreadsRequest\x1a\".popular.GetPopularThreadsResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/popular/threads\x12x\n" + "\x12GetPopularComments\x12\".popular.GetPopularCommentsRequest\x1a#.popular.GetPopularCommentsResponse\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/popular/commentsB\x1bZ\x19gen/popular-service/pb;pbb\x06proto3" @@ -258,16 +260,19 @@ var file_popular_service_proto_goTypes = []any{ (*GetPopularCommentsResponse)(nil), // 3: popular.GetPopularCommentsResponse (*pb.Thread)(nil), // 4: models.Thread (*pb.Comment)(nil), // 5: models.Comment + (*emptypb.Empty)(nil), // 6: google.protobuf.Empty } var file_popular_service_proto_depIdxs = []int32{ 4, // 0: popular.GetPopularThreadsResponse.threads:type_name -> models.Thread 5, // 1: popular.GetPopularCommentsResponse.comments:type_name -> models.Comment - 0, // 2: popular.PopularService.GetPopularThreads:input_type -> popular.GetPopularThreadsRequest - 2, // 3: popular.PopularService.GetPopularComments:input_type -> popular.GetPopularCommentsRequest - 1, // 4: popular.PopularService.GetPopularThreads:output_type -> popular.GetPopularThreadsResponse - 3, // 5: popular.PopularService.GetPopularComments:output_type -> popular.GetPopularCommentsResponse - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type + 6, // 2: popular.PopularService.CheckHealth:input_type -> google.protobuf.Empty + 0, // 3: popular.PopularService.GetPopularThreads:input_type -> popular.GetPopularThreadsRequest + 2, // 4: popular.PopularService.GetPopularComments:input_type -> popular.GetPopularCommentsRequest + 6, // 5: popular.PopularService.CheckHealth:output_type -> google.protobuf.Empty + 1, // 6: popular.PopularService.GetPopularThreads:output_type -> popular.GetPopularThreadsResponse + 3, // 7: popular.PopularService.GetPopularComments:output_type -> popular.GetPopularCommentsResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name diff --git a/code/gen/popular-service/pb/popular-service_grpc.pb.go b/code/gen/popular-service/pb/popular-service_grpc.pb.go index 10fe1c6..9a9c858 100644 --- a/code/gen/popular-service/pb/popular-service_grpc.pb.go +++ b/code/gen/popular-service/pb/popular-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: popular-service.proto package pb @@ -11,6 +11,7 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file @@ -19,6 +20,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + PopularService_CheckHealth_FullMethodName = "/popular.PopularService/CheckHealth" PopularService_GetPopularThreads_FullMethodName = "/popular.PopularService/GetPopularThreads" PopularService_GetPopularComments_FullMethodName = "/popular.PopularService/GetPopularComments" ) @@ -27,6 +29,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type PopularServiceClient interface { + CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) GetPopularThreads(ctx context.Context, in *GetPopularThreadsRequest, opts ...grpc.CallOption) (*GetPopularThreadsResponse, error) GetPopularComments(ctx context.Context, in *GetPopularCommentsRequest, opts ...grpc.CallOption) (*GetPopularCommentsResponse, error) } @@ -39,6 +42,16 @@ func NewPopularServiceClient(cc grpc.ClientConnInterface) PopularServiceClient { return &popularServiceClient{cc} } +func (c *popularServiceClient) CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, PopularService_CheckHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *popularServiceClient) GetPopularThreads(ctx context.Context, in *GetPopularThreadsRequest, opts ...grpc.CallOption) (*GetPopularThreadsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetPopularThreadsResponse) @@ -63,6 +76,7 @@ func (c *popularServiceClient) GetPopularComments(ctx context.Context, in *GetPo // All implementations must embed UnimplementedPopularServiceServer // for forward compatibility. type PopularServiceServer interface { + CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) GetPopularThreads(context.Context, *GetPopularThreadsRequest) (*GetPopularThreadsResponse, error) GetPopularComments(context.Context, *GetPopularCommentsRequest) (*GetPopularCommentsResponse, error) mustEmbedUnimplementedPopularServiceServer() @@ -75,6 +89,9 @@ type PopularServiceServer interface { // pointer dereference when methods are called. type UnimplementedPopularServiceServer struct{} +func (UnimplementedPopularServiceServer) CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented") +} func (UnimplementedPopularServiceServer) GetPopularThreads(context.Context, *GetPopularThreadsRequest) (*GetPopularThreadsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPopularThreads not implemented") } @@ -102,6 +119,24 @@ func RegisterPopularServiceServer(s grpc.ServiceRegistrar, srv PopularServiceSer s.RegisterService(&PopularService_ServiceDesc, srv) } +func _PopularService_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PopularServiceServer).CheckHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PopularService_CheckHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PopularServiceServer).CheckHealth(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _PopularService_GetPopularThreads_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetPopularThreadsRequest) if err := dec(in); err != nil { @@ -145,6 +180,10 @@ var PopularService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "popular.PopularService", HandlerType: (*PopularServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "CheckHealth", + Handler: _PopularService_CheckHealth_Handler, + }, { MethodName: "GetPopularThreads", Handler: _PopularService_GetPopularThreads_Handler, diff --git a/code/gen/search-service/pb/search-service.pb.go b/code/gen/search-service/pb/search-service.pb.go index 2235d50..42ef6b8 100644 --- a/code/gen/search-service/pb/search-service.pb.go +++ b/code/gen/search-service/pb/search-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: search-service.proto package pb @@ -11,6 +11,7 @@ import ( _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -227,7 +228,7 @@ var File_search_service_proto protoreflect.FileDescriptor const file_search_service_proto_rawDesc = "" + "\n" + - "\x14search-service.proto\x12\x06search\x1a\x1cgoogle/api/annotations.proto\x1a\fmodels.proto\"r\n" + + "\x14search-service.proto\x12\x06search\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/api/annotations.proto\x1a\fmodels.proto\"r\n" + "\rSearchRequest\x12\x14\n" + "\x05query\x18\x01 \x01(\tR\x05query\x12\x1b\n" + "\x06offset\x18\x02 \x01(\x05H\x00R\x06offset\x88\x01\x01\x12\x19\n" + @@ -240,8 +241,9 @@ const file_search_service_proto_rawDesc = "" + "\x17CommunitySearchResponse\x12+\n" + "\aresults\x18\x01 \x03(\v2\x11.models.CommunityR\aresults\"@\n" + "\x14ThreadSearchResponse\x12(\n" + - "\aresults\x18\x01 \x03(\v2\x0e.models.ThreadR\aresults2\xa8\x02\n" + - "\rSearchService\x12T\n" + + "\aresults\x18\x01 \x03(\v2\x0e.models.ThreadR\aresults2\xe7\x02\n" + + "\rSearchService\x12=\n" + + "\vCheckHealth\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12T\n" + "\fGlobalSearch\x12\x15.search.SearchRequest\x1a\x1c.search.GlobalSearchResponse\"\x0f\x82\xd3\xe4\x93\x02\t\x12\a/search\x12d\n" + "\x0fCommunitySearch\x12\x15.search.SearchRequest\x1a\x1f.search.CommunitySearchResponse\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/search/community\x12[\n" + "\fThreadSearch\x12\x15.search.SearchRequest\x1a\x1c.search.ThreadSearchResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/search/threadB\x1aZ\x18gen/search-service/pb;pbb\x06proto3" @@ -266,20 +268,23 @@ var file_search_service_proto_goTypes = []any{ (*ThreadSearchResponse)(nil), // 3: search.ThreadSearchResponse (*pb.Thread)(nil), // 4: models.Thread (*pb.Community)(nil), // 5: models.Community + (*emptypb.Empty)(nil), // 6: google.protobuf.Empty } var file_search_service_proto_depIdxs = []int32{ 4, // 0: search.GlobalSearchResponse.thread_results:type_name -> models.Thread 5, // 1: search.GlobalSearchResponse.community_results:type_name -> models.Community 5, // 2: search.CommunitySearchResponse.results:type_name -> models.Community 4, // 3: search.ThreadSearchResponse.results:type_name -> models.Thread - 0, // 4: search.SearchService.GlobalSearch:input_type -> search.SearchRequest - 0, // 5: search.SearchService.CommunitySearch:input_type -> search.SearchRequest - 0, // 6: search.SearchService.ThreadSearch:input_type -> search.SearchRequest - 1, // 7: search.SearchService.GlobalSearch:output_type -> search.GlobalSearchResponse - 2, // 8: search.SearchService.CommunitySearch:output_type -> search.CommunitySearchResponse - 3, // 9: search.SearchService.ThreadSearch:output_type -> search.ThreadSearchResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type + 6, // 4: search.SearchService.CheckHealth:input_type -> google.protobuf.Empty + 0, // 5: search.SearchService.GlobalSearch:input_type -> search.SearchRequest + 0, // 6: search.SearchService.CommunitySearch:input_type -> search.SearchRequest + 0, // 7: search.SearchService.ThreadSearch:input_type -> search.SearchRequest + 6, // 8: search.SearchService.CheckHealth:output_type -> google.protobuf.Empty + 1, // 9: search.SearchService.GlobalSearch:output_type -> search.GlobalSearchResponse + 2, // 10: search.SearchService.CommunitySearch:output_type -> search.CommunitySearchResponse + 3, // 11: search.SearchService.ThreadSearch:output_type -> search.ThreadSearchResponse + 8, // [8:12] is the sub-list for method output_type + 4, // [4:8] is the sub-list for method input_type 4, // [4:4] is the sub-list for extension type_name 4, // [4:4] is the sub-list for extension extendee 0, // [0:4] is the sub-list for field type_name diff --git a/code/gen/search-service/pb/search-service_grpc.pb.go b/code/gen/search-service/pb/search-service_grpc.pb.go index 96cc60b..cf288ce 100644 --- a/code/gen/search-service/pb/search-service_grpc.pb.go +++ b/code/gen/search-service/pb/search-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: search-service.proto package pb @@ -11,6 +11,7 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file @@ -19,6 +20,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + SearchService_CheckHealth_FullMethodName = "/search.SearchService/CheckHealth" SearchService_GlobalSearch_FullMethodName = "/search.SearchService/GlobalSearch" SearchService_CommunitySearch_FullMethodName = "/search.SearchService/CommunitySearch" SearchService_ThreadSearch_FullMethodName = "/search.SearchService/ThreadSearch" @@ -28,6 +30,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type SearchServiceClient interface { + CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) GlobalSearch(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*GlobalSearchResponse, error) CommunitySearch(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*CommunitySearchResponse, error) ThreadSearch(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*ThreadSearchResponse, error) @@ -41,6 +44,16 @@ func NewSearchServiceClient(cc grpc.ClientConnInterface) SearchServiceClient { return &searchServiceClient{cc} } +func (c *searchServiceClient) CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, SearchService_CheckHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *searchServiceClient) GlobalSearch(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*GlobalSearchResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GlobalSearchResponse) @@ -75,6 +88,7 @@ func (c *searchServiceClient) ThreadSearch(ctx context.Context, in *SearchReques // All implementations must embed UnimplementedSearchServiceServer // for forward compatibility. type SearchServiceServer interface { + CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) GlobalSearch(context.Context, *SearchRequest) (*GlobalSearchResponse, error) CommunitySearch(context.Context, *SearchRequest) (*CommunitySearchResponse, error) ThreadSearch(context.Context, *SearchRequest) (*ThreadSearchResponse, error) @@ -88,6 +102,9 @@ type SearchServiceServer interface { // pointer dereference when methods are called. type UnimplementedSearchServiceServer struct{} +func (UnimplementedSearchServiceServer) CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented") +} func (UnimplementedSearchServiceServer) GlobalSearch(context.Context, *SearchRequest) (*GlobalSearchResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GlobalSearch not implemented") } @@ -118,6 +135,24 @@ func RegisterSearchServiceServer(s grpc.ServiceRegistrar, srv SearchServiceServe s.RegisterService(&SearchService_ServiceDesc, srv) } +func _SearchService_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SearchServiceServer).CheckHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SearchService_CheckHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SearchServiceServer).CheckHealth(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _SearchService_GlobalSearch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SearchRequest) if err := dec(in); err != nil { @@ -179,6 +214,10 @@ var SearchService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "search.SearchService", HandlerType: (*SearchServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "CheckHealth", + Handler: _SearchService_CheckHealth_Handler, + }, { MethodName: "GlobalSearch", Handler: _SearchService_GlobalSearch_Handler, diff --git a/code/gen/thread-service/pb/thread-service.pb.go b/code/gen/thread-service/pb/thread-service.pb.go index c180d50..5d72480 100644 --- a/code/gen/thread-service/pb/thread-service.pb.go +++ b/code/gen/thread-service/pb/thread-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: thread-service.proto package pb @@ -452,8 +452,9 @@ const file_thread_service_proto_rawDesc = "" + "\f_vote_offsetB\x16\n" + "\x14_num_comments_offset\"%\n" + "\x13DeleteThreadRequest\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id2\xd2\x03\n" + - "\rThreadService\x12X\n" + + "\x02id\x18\x01 \x01(\tR\x02id2\x91\x04\n" + + "\rThreadService\x12=\n" + + "\vCheckHealth\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12X\n" + "\vListThreads\x12\x1a.thread.ListThreadsRequest\x1a\x1b.thread.ListThreadsResponse\"\x10\x82\xd3\xe4\x93\x02\n" + "\x12\b/threads\x12^\n" + "\fCreateThread\x12\x1b.thread.CreateThreadRequest\x1a\x1c.thread.CreateThreadResponse\"\x13\x82\xd3\xe4\x93\x02\r:\x01*\"\b/threads\x12L\n" + @@ -487,18 +488,20 @@ var file_thread_service_proto_goTypes = []any{ } var file_thread_service_proto_depIdxs = []int32{ 7, // 0: thread.ListThreadsResponse.threads:type_name -> models.Thread - 0, // 1: thread.ThreadService.ListThreads:input_type -> thread.ListThreadsRequest - 2, // 2: thread.ThreadService.CreateThread:input_type -> thread.CreateThreadRequest - 4, // 3: thread.ThreadService.GetThread:input_type -> thread.GetThreadRequest - 5, // 4: thread.ThreadService.UpdateThread:input_type -> thread.UpdateThreadRequest - 6, // 5: thread.ThreadService.DeleteThread:input_type -> thread.DeleteThreadRequest - 1, // 6: thread.ThreadService.ListThreads:output_type -> thread.ListThreadsResponse - 3, // 7: thread.ThreadService.CreateThread:output_type -> thread.CreateThreadResponse - 7, // 8: thread.ThreadService.GetThread:output_type -> models.Thread - 8, // 9: thread.ThreadService.UpdateThread:output_type -> google.protobuf.Empty - 8, // 10: thread.ThreadService.DeleteThread:output_type -> google.protobuf.Empty - 6, // [6:11] is the sub-list for method output_type - 1, // [1:6] is the sub-list for method input_type + 8, // 1: thread.ThreadService.CheckHealth:input_type -> google.protobuf.Empty + 0, // 2: thread.ThreadService.ListThreads:input_type -> thread.ListThreadsRequest + 2, // 3: thread.ThreadService.CreateThread:input_type -> thread.CreateThreadRequest + 4, // 4: thread.ThreadService.GetThread:input_type -> thread.GetThreadRequest + 5, // 5: thread.ThreadService.UpdateThread:input_type -> thread.UpdateThreadRequest + 6, // 6: thread.ThreadService.DeleteThread:input_type -> thread.DeleteThreadRequest + 8, // 7: thread.ThreadService.CheckHealth:output_type -> google.protobuf.Empty + 1, // 8: thread.ThreadService.ListThreads:output_type -> thread.ListThreadsResponse + 3, // 9: thread.ThreadService.CreateThread:output_type -> thread.CreateThreadResponse + 7, // 10: thread.ThreadService.GetThread:output_type -> models.Thread + 8, // 11: thread.ThreadService.UpdateThread:output_type -> google.protobuf.Empty + 8, // 12: thread.ThreadService.DeleteThread:output_type -> google.protobuf.Empty + 7, // [7:13] is the sub-list for method output_type + 1, // [1:7] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name diff --git a/code/gen/thread-service/pb/thread-service_grpc.pb.go b/code/gen/thread-service/pb/thread-service_grpc.pb.go index 38c4a47..8fe8cfe 100644 --- a/code/gen/thread-service/pb/thread-service_grpc.pb.go +++ b/code/gen/thread-service/pb/thread-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: thread-service.proto package pb @@ -21,6 +21,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + ThreadService_CheckHealth_FullMethodName = "/thread.ThreadService/CheckHealth" ThreadService_ListThreads_FullMethodName = "/thread.ThreadService/ListThreads" ThreadService_CreateThread_FullMethodName = "/thread.ThreadService/CreateThread" ThreadService_GetThread_FullMethodName = "/thread.ThreadService/GetThread" @@ -32,6 +33,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type ThreadServiceClient interface { + CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) ListThreads(ctx context.Context, in *ListThreadsRequest, opts ...grpc.CallOption) (*ListThreadsResponse, error) CreateThread(ctx context.Context, in *CreateThreadRequest, opts ...grpc.CallOption) (*CreateThreadResponse, error) GetThread(ctx context.Context, in *GetThreadRequest, opts ...grpc.CallOption) (*pb.Thread, error) @@ -47,6 +49,16 @@ func NewThreadServiceClient(cc grpc.ClientConnInterface) ThreadServiceClient { return &threadServiceClient{cc} } +func (c *threadServiceClient) CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ThreadService_CheckHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *threadServiceClient) ListThreads(ctx context.Context, in *ListThreadsRequest, opts ...grpc.CallOption) (*ListThreadsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListThreadsResponse) @@ -101,6 +113,7 @@ func (c *threadServiceClient) DeleteThread(ctx context.Context, in *DeleteThread // All implementations must embed UnimplementedThreadServiceServer // for forward compatibility. type ThreadServiceServer interface { + CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) ListThreads(context.Context, *ListThreadsRequest) (*ListThreadsResponse, error) CreateThread(context.Context, *CreateThreadRequest) (*CreateThreadResponse, error) GetThread(context.Context, *GetThreadRequest) (*pb.Thread, error) @@ -116,6 +129,9 @@ type ThreadServiceServer interface { // pointer dereference when methods are called. type UnimplementedThreadServiceServer struct{} +func (UnimplementedThreadServiceServer) CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented") +} func (UnimplementedThreadServiceServer) ListThreads(context.Context, *ListThreadsRequest) (*ListThreadsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListThreads not implemented") } @@ -152,6 +168,24 @@ func RegisterThreadServiceServer(s grpc.ServiceRegistrar, srv ThreadServiceServe s.RegisterService(&ThreadService_ServiceDesc, srv) } +func _ThreadService_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThreadServiceServer).CheckHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThreadService_CheckHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThreadServiceServer).CheckHealth(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _ThreadService_ListThreads_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListThreadsRequest) if err := dec(in); err != nil { @@ -249,6 +283,10 @@ var ThreadService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "thread.ThreadService", HandlerType: (*ThreadServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "CheckHealth", + Handler: _ThreadService_CheckHealth_Handler, + }, { MethodName: "ListThreads", Handler: _ThreadService_ListThreads_Handler, diff --git a/code/gen/vote-service/pb/vote-service.pb.go b/code/gen/vote-service/pb/vote-service.pb.go index 1d61b9c..9bbdbf7 100644 --- a/code/gen/vote-service/pb/vote-service.pb.go +++ b/code/gen/vote-service/pb/vote-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc v3.21.12 // source: vote-service.proto package pb @@ -120,8 +120,9 @@ const file_vote_service_proto_rawDesc = "" + "\tthread_id\x18\x01 \x01(\tR\bthreadId\"3\n" + "\x12VoteCommentRequest\x12\x1d\n" + "\n" + - "comment_id\x18\x01 \x01(\tR\tcommentId2\xc5\x03\n" + - "\vVoteService\x12h\n" + + "comment_id\x18\x01 \x01(\tR\tcommentId2\x84\x04\n" + + "\vVoteService\x12=\n" + + "\vCheckHealth\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12h\n" + "\fUpvoteThread\x12\x17.vote.VoteThreadRequest\x1a\x16.google.protobuf.Empty\"'\x82\xd3\xe4\x93\x02!:\x01*\"\x1c/votes/thread/{thread_id}/up\x12l\n" + "\x0eDownvoteThread\x12\x17.vote.VoteThreadRequest\x1a\x16.google.protobuf.Empty\")\x82\xd3\xe4\x93\x02#:\x01*\"\x1e/votes/thread/{thread_id}/down\x12l\n" + "\rUpvoteComment\x12\x18.vote.VoteCommentRequest\x1a\x16.google.protobuf.Empty\")\x82\xd3\xe4\x93\x02#:\x01*\"\x1e/votes/comment/{comment_id}/up\x12p\n" + @@ -146,16 +147,18 @@ var file_vote_service_proto_goTypes = []any{ (*emptypb.Empty)(nil), // 2: google.protobuf.Empty } var file_vote_service_proto_depIdxs = []int32{ - 0, // 0: vote.VoteService.UpvoteThread:input_type -> vote.VoteThreadRequest - 0, // 1: vote.VoteService.DownvoteThread:input_type -> vote.VoteThreadRequest - 1, // 2: vote.VoteService.UpvoteComment:input_type -> vote.VoteCommentRequest - 1, // 3: vote.VoteService.DownvoteComment:input_type -> vote.VoteCommentRequest - 2, // 4: vote.VoteService.UpvoteThread:output_type -> google.protobuf.Empty - 2, // 5: vote.VoteService.DownvoteThread:output_type -> google.protobuf.Empty - 2, // 6: vote.VoteService.UpvoteComment:output_type -> google.protobuf.Empty - 2, // 7: vote.VoteService.DownvoteComment:output_type -> google.protobuf.Empty - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type + 2, // 0: vote.VoteService.CheckHealth:input_type -> google.protobuf.Empty + 0, // 1: vote.VoteService.UpvoteThread:input_type -> vote.VoteThreadRequest + 0, // 2: vote.VoteService.DownvoteThread:input_type -> vote.VoteThreadRequest + 1, // 3: vote.VoteService.UpvoteComment:input_type -> vote.VoteCommentRequest + 1, // 4: vote.VoteService.DownvoteComment:input_type -> vote.VoteCommentRequest + 2, // 5: vote.VoteService.CheckHealth:output_type -> google.protobuf.Empty + 2, // 6: vote.VoteService.UpvoteThread:output_type -> google.protobuf.Empty + 2, // 7: vote.VoteService.DownvoteThread:output_type -> google.protobuf.Empty + 2, // 8: vote.VoteService.UpvoteComment:output_type -> google.protobuf.Empty + 2, // 9: vote.VoteService.DownvoteComment:output_type -> google.protobuf.Empty + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/code/gen/vote-service/pb/vote-service_grpc.pb.go b/code/gen/vote-service/pb/vote-service_grpc.pb.go index 9802f62..f2f3d10 100644 --- a/code/gen/vote-service/pb/vote-service_grpc.pb.go +++ b/code/gen/vote-service/pb/vote-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc v3.21.12 // source: vote-service.proto package pb @@ -20,6 +20,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + VoteService_CheckHealth_FullMethodName = "/vote.VoteService/CheckHealth" VoteService_UpvoteThread_FullMethodName = "/vote.VoteService/UpvoteThread" VoteService_DownvoteThread_FullMethodName = "/vote.VoteService/DownvoteThread" VoteService_UpvoteComment_FullMethodName = "/vote.VoteService/UpvoteComment" @@ -30,6 +31,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type VoteServiceClient interface { + CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) UpvoteThread(ctx context.Context, in *VoteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DownvoteThread(ctx context.Context, in *VoteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) UpvoteComment(ctx context.Context, in *VoteCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) @@ -44,6 +46,16 @@ func NewVoteServiceClient(cc grpc.ClientConnInterface) VoteServiceClient { return &voteServiceClient{cc} } +func (c *voteServiceClient) CheckHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, VoteService_CheckHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *voteServiceClient) UpvoteThread(ctx context.Context, in *VoteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -88,6 +100,7 @@ func (c *voteServiceClient) DownvoteComment(ctx context.Context, in *VoteComment // All implementations must embed UnimplementedVoteServiceServer // for forward compatibility. type VoteServiceServer interface { + CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) UpvoteThread(context.Context, *VoteThreadRequest) (*emptypb.Empty, error) DownvoteThread(context.Context, *VoteThreadRequest) (*emptypb.Empty, error) UpvoteComment(context.Context, *VoteCommentRequest) (*emptypb.Empty, error) @@ -102,6 +115,9 @@ type VoteServiceServer interface { // pointer dereference when methods are called. type UnimplementedVoteServiceServer struct{} +func (UnimplementedVoteServiceServer) CheckHealth(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented") +} func (UnimplementedVoteServiceServer) UpvoteThread(context.Context, *VoteThreadRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method UpvoteThread not implemented") } @@ -135,6 +151,24 @@ func RegisterVoteServiceServer(s grpc.ServiceRegistrar, srv VoteServiceServer) { s.RegisterService(&VoteService_ServiceDesc, srv) } +func _VoteService_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VoteServiceServer).CheckHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VoteService_CheckHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VoteServiceServer).CheckHealth(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _VoteService_UpvoteThread_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VoteThreadRequest) if err := dec(in); err != nil { @@ -214,6 +248,10 @@ var VoteService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "vote.VoteService", HandlerType: (*VoteServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "CheckHealth", + Handler: _VoteService_CheckHealth_Handler, + }, { MethodName: "UpvoteThread", Handler: _VoteService_UpvoteThread_Handler, diff --git a/code/grpc-gateway/main.go b/code/grpc-gateway/main.go index 71e7394..3f5bbfe 100644 --- a/code/grpc-gateway/main.go +++ b/code/grpc-gateway/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" commentpb "gen/comment-service/pb" communitypb "gen/community-service/pb" @@ -16,6 +17,7 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/emptypb" ) func getGrpcServerAddress(hostEnvVar string, portEnvVar string) string { @@ -30,6 +32,81 @@ func getGrpcServerAddress(hostEnvVar string, portEnvVar string) string { return fmt.Sprintf("%s:%s", host, port) } +func connectGrpcClient(hostEnvVar string, portEnvVar string) *grpc.ClientConn { + addr := getGrpcServerAddress(hostEnvVar, portEnvVar) + conn, err := grpc.NewClient(addr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(1024*1024*500), // 500MB + grpc.MaxCallSendMsgSize(1024*1024*500), // 500MB + ), + ) + if err != nil { + log.Fatalf("failed to connect to %s: %v", addr, err) + } + return conn +} + +func handleHealthCheck(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + health := map[string]bool{ + "community-service": false, + "thread-service": false, + "comment-service": false, + "vote-service": false, + "search-service": false, + "popular-service": false, + } + + communityConn := connectGrpcClient("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT") + defer communityConn.Close() + communityClient := communitypb.NewCommunityServiceClient(communityConn) + _, err := communityClient.CheckHealth(ctx, &emptypb.Empty{}) + health["community-service"] = err == nil + + threadConn := connectGrpcClient("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT") + defer threadConn.Close() + threadClient := threadpb.NewThreadServiceClient(threadConn) + _, err = threadClient.CheckHealth(ctx, &emptypb.Empty{}) + health["thread-service"] = err == nil + + commentConn := connectGrpcClient("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT") + defer commentConn.Close() + commentClient := commentpb.NewCommentServiceClient(commentConn) + _, err = commentClient.CheckHealth(ctx, &emptypb.Empty{}) + health["comment-service"] = err == nil + + voteConn := connectGrpcClient("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT") + defer voteConn.Close() + voteClient := votepb.NewVoteServiceClient(voteConn) + _, err = voteClient.CheckHealth(ctx, &emptypb.Empty{}) + health["vote-service"] = err == nil + + searchConn := connectGrpcClient("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT") + defer searchConn.Close() + searchClient := searchpb.NewSearchServiceClient(searchConn) + _, err = searchClient.CheckHealth(ctx, &emptypb.Empty{}) + health["search-service"] = err == nil + + popularConn := connectGrpcClient("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT") + defer popularConn.Close() + popularClient := popularpb.NewPopularServiceClient(popularConn) + _, err = popularClient.CheckHealth(ctx, &emptypb.Empty{}) + health["popular-service"] = err == nil + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + jsonResponse, err := json.Marshal(health) + if err != nil { + http.Error(w, "Failed to marshal JSON response", http.StatusInternalServerError) + return + } + + w.Write(jsonResponse) +} + func main() { gwmux := runtime.NewServeMux() @@ -71,6 +148,8 @@ func main() { log.Fatalf("Failed to register gRPC gateway: %v", err) } + http.HandleFunc("/health", handleHealthCheck) + http.Handle("/", gwmux) port := os.Getenv("GRPC_GATEWAY_PORT") diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 7ee716b..10ac07e 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -65,4 +65,14 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: POPULAR_SERVICE_PORT \ No newline at end of file + key: POPULAR_SERVICE_PORT + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 diff --git a/code/kubernetes/mongo/deployment.yaml b/code/kubernetes/mongo/deployment.yaml index 6f788ff..dd08d0c 100644 --- a/code/kubernetes/mongo/deployment.yaml +++ b/code/kubernetes/mongo/deployment.yaml @@ -36,6 +36,22 @@ spec: volumeMounts: - name: mongodb-data mountPath: /data/db + livenessProbe: + exec: + command: + - mongosh + - --eval + - "db.adminCommand('ping')" + initialDelaySeconds: 30 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - mongosh + - --eval + - "db.adminCommand('ping')" + initialDelaySeconds: 5 + timeoutSeconds: 5 volumes: - name: mongodb-data persistentVolumeClaim: diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 22f4dc4..3f43a0b 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -37,4 +37,12 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: THREAD_SERVICE_PORT \ No newline at end of file + key: THREAD_SERVICE_PORT + readinessProbe: + tcpSocket: + port: 50054 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50054 + initialDelaySeconds: 5 diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index b9817eb..976be0f 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -37,4 +37,12 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: THREAD_SERVICE_PORT \ No newline at end of file + key: THREAD_SERVICE_PORT + readinessProbe: + tcpSocket: + port: 50052 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50052 + initialDelaySeconds: 5 diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index 2b5038e..5a4f76b 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -35,6 +35,14 @@ spec: - mountPath: /var/secret/gcp/ name: gcs-credentials readOnly: true + readinessProbe: + tcpSocket: + port: 50051 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50051 + initialDelaySeconds: 5 volumes: - name: gcs-credentials secret: diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index 32ee19f..47f14a0 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -37,4 +37,12 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: COMMENT_SERVICE_PORT \ No newline at end of file + key: COMMENT_SERVICE_PORT + readinessProbe: + tcpSocket: + port: 50057 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50057 + initialDelaySeconds: 5 diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index 32b5aaf..a33f4ea 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -37,4 +37,12 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: THREAD_SERVICE_PORT \ No newline at end of file + key: THREAD_SERVICE_PORT + readinessProbe: + tcpSocket: + port: 50056 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50056 + initialDelaySeconds: 5 diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index 86800ac..e5891bb 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -37,4 +37,12 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: COMMUNITY_SERVICE_PORT \ No newline at end of file + key: COMMUNITY_SERVICE_PORT + readinessProbe: + tcpSocket: + port: 50053 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50053 + initialDelaySeconds: 5 diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index 8480665..fd828c8 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -37,4 +37,12 @@ spec: valueFrom: configMapKeyRef: name: threadit-config - key: COMMENT_SERVICE_PORT \ No newline at end of file + key: COMMENT_SERVICE_PORT + readinessProbe: + tcpSocket: + port: 50055 + initialDelaySeconds: 5 + livenessProbe: + tcpSocket: + port: 50055 + initialDelaySeconds: 5 diff --git a/code/proto/comment-service.proto b/code/proto/comment-service.proto index 8a0b13c..39653bb 100644 --- a/code/proto/comment-service.proto +++ b/code/proto/comment-service.proto @@ -9,6 +9,8 @@ import "google/api/annotations.proto"; import "models.proto"; service CommentService { + rpc CheckHealth(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ListComments(ListCommentsRequest) returns (ListCommentsResponse) { option (google.api.http) = { get: "/comments" diff --git a/code/proto/community-service.proto b/code/proto/community-service.proto index efcce95..093ff78 100644 --- a/code/proto/community-service.proto +++ b/code/proto/community-service.proto @@ -9,6 +9,8 @@ import "google/api/annotations.proto"; import "models.proto"; service CommunityService { + rpc CheckHealth(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ListCommunities(ListCommunitiesRequest) returns (ListCommunitiesResponse) { option (google.api.http) = { get: "/communities" diff --git a/code/proto/popular-service.proto b/code/proto/popular-service.proto index a228286..9a66d9d 100644 --- a/code/proto/popular-service.proto +++ b/code/proto/popular-service.proto @@ -4,15 +4,19 @@ package popular; option go_package = "gen/popular-service/pb;pb"; +import "google/protobuf/empty.proto"; import "google/api/annotations.proto"; import "models.proto"; service PopularService { + rpc CheckHealth(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc GetPopularThreads(GetPopularThreadsRequest) returns (GetPopularThreadsResponse) { option (google.api.http) = { get: "/popular/threads" }; } + rpc GetPopularComments(GetPopularCommentsRequest) returns (GetPopularCommentsResponse) { option (google.api.http) = { get: "/popular/comments" diff --git a/code/proto/search-service.proto b/code/proto/search-service.proto index 5756c05..81f94ed 100644 --- a/code/proto/search-service.proto +++ b/code/proto/search-service.proto @@ -4,10 +4,13 @@ package search; option go_package = "gen/search-service/pb;pb"; +import "google/protobuf/empty.proto"; import "google/api/annotations.proto"; import "models.proto"; service SearchService { + rpc CheckHealth(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc GlobalSearch (SearchRequest) returns (GlobalSearchResponse) { option (google.api.http) = { get: "/search" diff --git a/code/proto/thread-service.proto b/code/proto/thread-service.proto index e069bac..320610f 100644 --- a/code/proto/thread-service.proto +++ b/code/proto/thread-service.proto @@ -9,6 +9,8 @@ import "google/api/annotations.proto"; import "models.proto"; service ThreadService { + rpc CheckHealth(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ListThreads (ListThreadsRequest) returns (ListThreadsResponse) { option (google.api.http) = { get: "/threads" diff --git a/code/proto/vote-service.proto b/code/proto/vote-service.proto index f634eaf..27a4a88 100644 --- a/code/proto/vote-service.proto +++ b/code/proto/vote-service.proto @@ -8,6 +8,8 @@ import "google/protobuf/empty.proto"; import "google/api/annotations.proto"; service VoteService { + rpc CheckHealth(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc UpvoteThread (VoteThreadRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/votes/thread/{thread_id}/up" diff --git a/code/services/comment-service/src/server.go b/code/services/comment-service/src/server.go index e906b70..6eabede 100644 --- a/code/services/comment-service/src/server.go +++ b/code/services/comment-service/src/server.go @@ -23,6 +23,10 @@ const ( MaxCommentLength = 500 ) +func (s *CommentServer) CheckHealth(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + func (s *CommentServer) ListComments(ctx context.Context, req *commentpb.ListCommentsRequest) (*commentpb.ListCommentsResponse, error) { // validate inputs if req.ThreadId != nil && req.GetThreadId() == "" { diff --git a/code/services/community-service/src/server.go b/code/services/community-service/src/server.go index fe5e330..17eee01 100644 --- a/code/services/community-service/src/server.go +++ b/code/services/community-service/src/server.go @@ -6,10 +6,11 @@ import ( dbpb "gen/db-service/pb" models "gen/models/pb" threadpb "gen/thread-service/pb" + "math" + "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" - "math" ) type CommunityServer struct { @@ -23,6 +24,10 @@ const ( MaxLength = 50 ) +func (s *CommunityServer) CheckHealth(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + func (s *CommunityServer) ListCommunities(ctx context.Context, req *communitypb.ListCommunitiesRequest) (*communitypb.ListCommunitiesResponse, error) { // validate inputs if req.Offset != nil && req.GetOffset() < 0 { diff --git a/code/services/popular-service/src/server.go b/code/services/popular-service/src/server.go index 2274701..a7e0788 100644 --- a/code/services/popular-service/src/server.go +++ b/code/services/popular-service/src/server.go @@ -8,6 +8,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" ) type PopularServer struct { @@ -16,6 +17,10 @@ type PopularServer struct { CommentClient commentpb.CommentServiceClient } +func (s *PopularServer) CheckHealth(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + func (s *PopularServer) GetPopularThreads(ctx context.Context, req *popularpb.GetPopularThreadsRequest) (*popularpb.GetPopularThreadsResponse, error) { // validate inputs if req.Offset != nil && req.GetOffset() < 0 { diff --git a/code/services/search-service/src/server.go b/code/services/search-service/src/server.go index 8ccfaa6..b0248f1 100644 --- a/code/services/search-service/src/server.go +++ b/code/services/search-service/src/server.go @@ -6,8 +6,10 @@ import ( models "gen/models/pb" searchpb "gen/search-service/pb" threadpb "gen/thread-service/pb" + "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" ) type SearchServer struct { @@ -16,6 +18,10 @@ type SearchServer struct { ThreadClient threadpb.ThreadServiceClient } +func (s *SearchServer) CheckHealth(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + func (s *SearchServer) GlobalSearch(ctx context.Context, req *searchpb.SearchRequest) (*searchpb.GlobalSearchResponse, error) { // validate inputs reqErr := validateSearchRequest(req) diff --git a/code/services/thread-service/src/server.go b/code/services/thread-service/src/server.go index 6b2c1cd..2e334a7 100644 --- a/code/services/thread-service/src/server.go +++ b/code/services/thread-service/src/server.go @@ -26,6 +26,10 @@ const ( MinContentLength = 3 ) +func (s *ThreadServer) CheckHealth(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + func (s *ThreadServer) ListThreads(ctx context.Context, req *threadpb.ListThreadsRequest) (*threadpb.ListThreadsResponse, error) { // validate inputs if req.CommunityId != nil && req.GetCommunityId() == "" { diff --git a/code/services/vote-service/src/server.go b/code/services/vote-service/src/server.go index 610841c..966db00 100644 --- a/code/services/vote-service/src/server.go +++ b/code/services/vote-service/src/server.go @@ -5,6 +5,7 @@ import ( commentpb "gen/comment-service/pb" threadpb "gen/thread-service/pb" votepb "gen/vote-service/pb" + "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" @@ -16,6 +17,10 @@ type VoteServer struct { CommentClient commentpb.CommentServiceClient } +func (s *VoteServer) CheckHealth(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + func (s *VoteServer) UpvoteThread(ctx context.Context, req *votepb.VoteThreadRequest) (*emptypb.Empty, error) { return s.updateThreadVote(ctx, req, 1) } diff --git a/code/traefik/dynamic.yml b/code/traefik/dynamic.yml index c98bbe5..d679a0f 100644 --- a/code/traefik/dynamic.yml +++ b/code/traefik/dynamic.yml @@ -61,6 +61,14 @@ http: - "cors" - "api-strip-prefix" + grpc-gateway: + rule: "PathPrefix(`/api`)" + service: "gprc-gateway" + entryPoints: ["web"] + middlewares: + - "cors" + - "api-strip-prefix" + services: community-service: loadBalancer: @@ -91,3 +99,8 @@ http: loadBalancer: servers: - url: "http://grpc-gateway:8080/popular" + + gprc-gateway: + loadBalancer: + servers: + - url: "http://grpc-gateway:8080" From 9167c24f15db4a665d105fac1b7c3c07ebda5bc1 Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Sun, 18 May 2025 18:54:02 +0100 Subject: [PATCH 05/18] Update main.go --- code/grpc-gateway/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/grpc-gateway/main.go b/code/grpc-gateway/main.go index 39017b2..b72e74e 100644 --- a/code/grpc-gateway/main.go +++ b/code/grpc-gateway/main.go @@ -60,7 +60,7 @@ func main() { } // Register gRPC-Gateway routes with auth middleware - httpMux.Handle("/api/v1/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpMux.Handle("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Auth middleware for API routes authMiddleware := middleware.NewAuthMiddleware(middleware.KeycloakConfig{ Realm: os.Getenv("KEYCLOAK_REALM"), From eb16cb3fc0faecb2fcfd2deb03e69082bbe87d2b Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Sun, 18 May 2025 18:54:02 +0100 Subject: [PATCH 06/18] Minor Changes --- code/grpc-gateway/main.go | 2 +- code/grpc-gateway/middleware/auth_routes.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/grpc-gateway/main.go b/code/grpc-gateway/main.go index 39017b2..b72e74e 100644 --- a/code/grpc-gateway/main.go +++ b/code/grpc-gateway/main.go @@ -60,7 +60,7 @@ func main() { } // Register gRPC-Gateway routes with auth middleware - httpMux.Handle("/api/v1/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpMux.Handle("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Auth middleware for API routes authMiddleware := middleware.NewAuthMiddleware(middleware.KeycloakConfig{ Realm: os.Getenv("KEYCLOAK_REALM"), diff --git a/code/grpc-gateway/middleware/auth_routes.go b/code/grpc-gateway/middleware/auth_routes.go index a81e597..0c2799d 100644 --- a/code/grpc-gateway/middleware/auth_routes.go +++ b/code/grpc-gateway/middleware/auth_routes.go @@ -51,7 +51,7 @@ func (h *AuthHandler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/auth/logout", h.handleLogout) } -func (h *AuthHandler) validateRegistration(req *RegisterRequest) error { +func (h *AuthHandler) validateRegister(req *RegisterRequest) error { // Username validation if len(req.Username) < 3 || len(req.Username) > 30 { return fmt.Errorf("username must be between 3 and 30 characters") @@ -96,7 +96,7 @@ func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) { } // Validate registration data - if err := h.validateRegistration(&req); err != nil { + if err := h.validateRegister(&req); err != nil { h.sendError(w, err.Error(), http.StatusBadRequest) return } From f093908c1d53aa2ec7fb89e3f6ec8d677733f04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 20 May 2025 03:46:55 +0100 Subject: [PATCH 07/18] Updated proto generated code --- code/gen/comment-service/pb/comment-service.pb.go | 2 +- code/gen/comment-service/pb/comment-service_grpc.pb.go | 2 +- code/gen/community-service/pb/community-service.pb.go | 2 +- code/gen/community-service/pb/community-service_grpc.pb.go | 2 +- code/gen/db-service/pb/db-service.pb.go | 2 +- code/gen/db-service/pb/db-service_grpc.pb.go | 2 +- code/gen/models/pb/models.pb.go | 2 +- code/gen/popular-service/pb/popular-service.pb.go | 2 +- code/gen/popular-service/pb/popular-service_grpc.pb.go | 2 +- code/gen/search-service/pb/search-service.pb.go | 2 +- code/gen/search-service/pb/search-service_grpc.pb.go | 2 +- code/gen/thread-service/pb/thread-service.pb.go | 2 +- code/gen/thread-service/pb/thread-service_grpc.pb.go | 2 +- code/gen/vote-service/pb/vote-service.pb.go | 2 +- code/gen/vote-service/pb/vote-service_grpc.pb.go | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/code/gen/comment-service/pb/comment-service.pb.go b/code/gen/comment-service/pb/comment-service.pb.go index 7664ecb..36b53cd 100644 --- a/code/gen/comment-service/pb/comment-service.pb.go +++ b/code/gen/comment-service/pb/comment-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: comment-service.proto package pb diff --git a/code/gen/comment-service/pb/comment-service_grpc.pb.go b/code/gen/comment-service/pb/comment-service_grpc.pb.go index 5923000..549d86b 100644 --- a/code/gen/comment-service/pb/comment-service_grpc.pb.go +++ b/code/gen/comment-service/pb/comment-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: comment-service.proto package pb diff --git a/code/gen/community-service/pb/community-service.pb.go b/code/gen/community-service/pb/community-service.pb.go index 8de473a..54d8c03 100644 --- a/code/gen/community-service/pb/community-service.pb.go +++ b/code/gen/community-service/pb/community-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: community-service.proto package pb diff --git a/code/gen/community-service/pb/community-service_grpc.pb.go b/code/gen/community-service/pb/community-service_grpc.pb.go index a36beaf..088a329 100644 --- a/code/gen/community-service/pb/community-service_grpc.pb.go +++ b/code/gen/community-service/pb/community-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: community-service.proto package pb diff --git a/code/gen/db-service/pb/db-service.pb.go b/code/gen/db-service/pb/db-service.pb.go index a54a036..b6e4724 100644 --- a/code/gen/db-service/pb/db-service.pb.go +++ b/code/gen/db-service/pb/db-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: db-service.proto package pb diff --git a/code/gen/db-service/pb/db-service_grpc.pb.go b/code/gen/db-service/pb/db-service_grpc.pb.go index 8f97d76..854c780 100644 --- a/code/gen/db-service/pb/db-service_grpc.pb.go +++ b/code/gen/db-service/pb/db-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: db-service.proto package pb diff --git a/code/gen/models/pb/models.pb.go b/code/gen/models/pb/models.pb.go index 9659e27..8bf0cf9 100644 --- a/code/gen/models/pb/models.pb.go +++ b/code/gen/models/pb/models.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: models.proto package models diff --git a/code/gen/popular-service/pb/popular-service.pb.go b/code/gen/popular-service/pb/popular-service.pb.go index 0cd3dcb..79e095a 100644 --- a/code/gen/popular-service/pb/popular-service.pb.go +++ b/code/gen/popular-service/pb/popular-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: popular-service.proto package pb diff --git a/code/gen/popular-service/pb/popular-service_grpc.pb.go b/code/gen/popular-service/pb/popular-service_grpc.pb.go index 9a9c858..48c75e5 100644 --- a/code/gen/popular-service/pb/popular-service_grpc.pb.go +++ b/code/gen/popular-service/pb/popular-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: popular-service.proto package pb diff --git a/code/gen/search-service/pb/search-service.pb.go b/code/gen/search-service/pb/search-service.pb.go index 42ef6b8..7b8ab6c 100644 --- a/code/gen/search-service/pb/search-service.pb.go +++ b/code/gen/search-service/pb/search-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: search-service.proto package pb diff --git a/code/gen/search-service/pb/search-service_grpc.pb.go b/code/gen/search-service/pb/search-service_grpc.pb.go index cf288ce..a80e8a4 100644 --- a/code/gen/search-service/pb/search-service_grpc.pb.go +++ b/code/gen/search-service/pb/search-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: search-service.proto package pb diff --git a/code/gen/thread-service/pb/thread-service.pb.go b/code/gen/thread-service/pb/thread-service.pb.go index 5d72480..e0e60a9 100644 --- a/code/gen/thread-service/pb/thread-service.pb.go +++ b/code/gen/thread-service/pb/thread-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: thread-service.proto package pb diff --git a/code/gen/thread-service/pb/thread-service_grpc.pb.go b/code/gen/thread-service/pb/thread-service_grpc.pb.go index 8fe8cfe..1a29600 100644 --- a/code/gen/thread-service/pb/thread-service_grpc.pb.go +++ b/code/gen/thread-service/pb/thread-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: thread-service.proto package pb diff --git a/code/gen/vote-service/pb/vote-service.pb.go b/code/gen/vote-service/pb/vote-service.pb.go index 9bbdbf7..aea8bc4 100644 --- a/code/gen/vote-service/pb/vote-service.pb.go +++ b/code/gen/vote-service/pb/vote-service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc v5.29.4 // source: vote-service.proto package pb diff --git a/code/gen/vote-service/pb/vote-service_grpc.pb.go b/code/gen/vote-service/pb/vote-service_grpc.pb.go index f2f3d10..5a345ec 100644 --- a/code/gen/vote-service/pb/vote-service_grpc.pb.go +++ b/code/gen/vote-service/pb/vote-service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v5.29.4 // source: vote-service.proto package pb From 350145a34b465a891acf848604ad96f93d02f6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 20 May 2025 05:04:14 +0100 Subject: [PATCH 08/18] Updated probes configuration --- code/kubernetes/grpc-gateway/deployment.yaml | 6 ++++-- code/kubernetes/mongo/deployment.yaml | 4 ++-- code/kubernetes/services/comment-service/deployment.yaml | 6 ++++-- code/kubernetes/services/community-service/deployment.yaml | 6 ++++-- code/kubernetes/services/db-service/deployment.yaml | 6 ++++-- code/kubernetes/services/popular-service/deployment.yaml | 6 ++++-- code/kubernetes/services/search-service/deployment.yaml | 6 ++++-- code/kubernetes/services/thread-service/deployment.yaml | 6 ++++-- code/kubernetes/services/vote-service/deployment.yaml | 6 ++++-- 9 files changed, 34 insertions(+), 18 deletions(-) diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 10ac07e..d45954b 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -70,9 +70,11 @@ spec: httpGet: path: /health port: 8080 - initialDelaySeconds: 5 + initialDelaySeconds: 10 + timeoutSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 - initialDelaySeconds: 5 + initialDelaySeconds: 10 + timeoutSeconds: 5 diff --git a/code/kubernetes/mongo/deployment.yaml b/code/kubernetes/mongo/deployment.yaml index 5a14967..bb1713a 100644 --- a/code/kubernetes/mongo/deployment.yaml +++ b/code/kubernetes/mongo/deployment.yaml @@ -42,7 +42,7 @@ spec: - mongosh - --eval - "db.adminCommand('ping')" - initialDelaySeconds: 30 + initialDelaySeconds: 15 timeoutSeconds: 5 readinessProbe: exec: @@ -50,7 +50,7 @@ spec: - mongosh - --eval - "db.adminCommand('ping')" - initialDelaySeconds: 5 + initialDelaySeconds: 15 timeoutSeconds: 5 volumes: - name: mongodb-data diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 3f43a0b..9b02733 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -41,8 +41,10 @@ spec: readinessProbe: tcpSocket: port: 50054 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 livenessProbe: tcpSocket: port: 50054 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index 976be0f..0825b7c 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -41,8 +41,10 @@ spec: readinessProbe: tcpSocket: port: 50052 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 livenessProbe: tcpSocket: port: 50052 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index c976dcf..29693c6 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -38,11 +38,13 @@ spec: readinessProbe: tcpSocket: port: 50051 - initialDelaySeconds: 5 + initialDelaySeconds: 8 + timeoutSeconds: 5 livenessProbe: tcpSocket: port: 50051 - initialDelaySeconds: 5 + initialDelaySeconds: 8 + timeoutSeconds: 5 volumes: - name: bucket-credentials secret: diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index 47f14a0..fbd2b5c 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -41,8 +41,10 @@ spec: readinessProbe: tcpSocket: port: 50057 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 livenessProbe: tcpSocket: port: 50057 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index a33f4ea..9b7cb3b 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -41,8 +41,10 @@ spec: readinessProbe: tcpSocket: port: 50056 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 livenessProbe: tcpSocket: port: 50056 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index e5891bb..4092890 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -41,8 +41,10 @@ spec: readinessProbe: tcpSocket: port: 50053 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 livenessProbe: tcpSocket: port: 50053 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index fd828c8..e19959e 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -41,8 +41,10 @@ spec: readinessProbe: tcpSocket: port: 50055 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 livenessProbe: tcpSocket: port: 50055 - initialDelaySeconds: 5 + initialDelaySeconds: 6 + timeoutSeconds: 4 From 51b06656554b23b6e6011c146e8389523e8be174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 20 May 2025 05:14:28 +0100 Subject: [PATCH 09/18] Corrected typo in README.md --- code/kubernetes/scripts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/kubernetes/scripts/README.md b/code/kubernetes/scripts/README.md index ad025b0..0716185 100644 --- a/code/kubernetes/scripts/README.md +++ b/code/kubernetes/scripts/README.md @@ -43,8 +43,8 @@ $ ./cluster-info.sh - `--pods` Shows the status and details of all pods running in the specified namespace. - `--services` Lists all services deployed in the specified namespace. - `--deployments` Shows deployment configurations and statuses for the namespace. -- `--resource-pods` Displays real-time CPU and memory usage metrics for each pod. -- `--resource-nodes` Displays real-time CPU and memory usage metrics for each node in the cluster. +- `--resources-pods` Displays real-time CPU and memory usage metrics for each pod. +- `--resources-nodes` Displays real-time CPU and memory usage metrics for each node in the cluster. - `--all` Runs all of the above commands to display full cluster info. ### 4. Delete Cluster From 60b9937ec63e4966b44161069cf37ef0b4f19c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 20 May 2025 06:06:59 +0100 Subject: [PATCH 10/18] Updated resources requests and limits --- code/kubernetes/mongo/deployment.yaml | 8 ++--- .../services/comment-service/deployment.yaml | 8 ++--- .../community-service/deployment.yaml | 8 ++--- .../services/db-service/deployment.yaml | 8 ++--- .../services/popular-service/deployment.yaml | 8 ++--- .../services/search-service/deployment.yaml | 8 ++--- .../services/thread-service/deployment.yaml | 8 ++--- .../services/vote-service/deployment.yaml | 8 ++--- code/kubernetes/traefik/values.yaml | 33 ++++++++++++------- 9 files changed, 54 insertions(+), 43 deletions(-) diff --git a/code/kubernetes/mongo/deployment.yaml b/code/kubernetes/mongo/deployment.yaml index 06c4fae..a68ce41 100644 --- a/code/kubernetes/mongo/deployment.yaml +++ b/code/kubernetes/mongo/deployment.yaml @@ -19,11 +19,11 @@ spec: - containerPort: 27017 resources: requests: - cpu: "300m" - memory: "512Mi" + cpu: 150m + memory: 700Mi limits: - cpu: "700m" - memory: "1Gi" + cpu: 500m + memory: 1Gi env: - name: MONGO_INITDB_DATABASE valueFrom: diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 8256ced..7d057f7 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50054 resources: requests: - cpu: "100m" - memory: "128Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "300m" - memory: "384Mi" + cpu: 50m + memory: 100Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index 5b96274..5e42b86 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50052 resources: requests: - cpu: "100m" - memory: "128Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "300m" - memory: "384Mi" + cpu: 50m + memory: 100Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index fae6b6e..12621a9 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50051 resources: requests: - cpu: "200m" - memory: "256Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "500m" - memory: "512Mi" + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index 05e4c41..257a2e2 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50057 resources: requests: - cpu: "100m" - memory: "128Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "300m" - memory: "384Mi" + cpu: 50m + memory: 100Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index dc4b7d5..a98cd31 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50056 resources: requests: - cpu: "150m" - memory: "192Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "400m" - memory: "448Mi" + cpu: 50m + memory: 100Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index e115b0c..6eadf1c 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50053 resources: requests: - cpu: "150m" - memory: "192Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "400m" - memory: "448Mi" + cpu: 50m + memory: 100Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index 6343aa8..0ea3a85 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50055 resources: requests: - cpu: "100m" - memory: "128Mi" + cpu: 10m + memory: 20Mi limits: - cpu: "250m" - memory: "256Mi" + cpu: 50m + memory: 100Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/traefik/values.yaml b/code/kubernetes/traefik/values.yaml index 9f28b52..504a34c 100644 --- a/code/kubernetes/traefik/values.yaml +++ b/code/kubernetes/traefik/values.yaml @@ -1,12 +1,23 @@ -# https://github.com/traefik/traefik-helm-chart/blob/master/traefik/VALUES.md +# traefik/values.yaml -# autoscaling: # TODO: If needed, uncomment and configure HPA settings -# enabled: true -# maxReplicas: 2 -# metrics: -# - type: Resource -# resource: -# name: cpu -# target: -# type: Utilization -# averageUtilization: 60 +replicaCount: 1 + +resources: + requests: + cpu: 20m + memory: 40Mi + limits: + cpu: 100m + memory: 100Mi + +# TODO: If needed, uncomment and configure HPA settings +#autoscaling: +# enabled: true +# maxReplicas: 2 +# metrics: +# - type: Resource +# resource: +# name: cpu +# target: +# type: Utilization +# averageUtilization: 60 \ No newline at end of file From 3319349b0fd9170aa5668ad8f825bc8822fbfb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 20 May 2025 06:35:05 +0100 Subject: [PATCH 11/18] Updated hpa configuration --- code/kubernetes/grpc-gateway/deployment.yaml | 19 +++++++++++++++++++ code/kubernetes/hpa/comment-service-hpa.yaml | 18 ------------------ .../kubernetes/hpa/community-service-hpa.yaml | 18 ------------------ code/kubernetes/hpa/grpc-gateway-hpa.yaml | 18 ------------------ code/kubernetes/hpa/popular-service-hpa.yaml | 18 ------------------ code/kubernetes/hpa/search-service-hpa.yaml | 18 ------------------ code/kubernetes/hpa/thread-service-hpa.yaml | 18 ------------------ code/kubernetes/hpa/vote-service-hpa.yaml | 18 ------------------ .../services/comment-service/deployment.yaml | 19 +++++++++++++++++++ .../community-service/deployment.yaml | 19 +++++++++++++++++++ .../services/db-service/deployment.yaml | 19 +++++++++++++++++++ .../services/popular-service/deployment.yaml | 19 +++++++++++++++++++ .../services/search-service/deployment.yaml | 19 +++++++++++++++++++ .../services/thread-service/deployment.yaml | 19 +++++++++++++++++++ .../services/vote-service/deployment.yaml | 19 +++++++++++++++++++ 15 files changed, 152 insertions(+), 126 deletions(-) delete mode 100644 code/kubernetes/hpa/comment-service-hpa.yaml delete mode 100644 code/kubernetes/hpa/community-service-hpa.yaml delete mode 100644 code/kubernetes/hpa/grpc-gateway-hpa.yaml delete mode 100644 code/kubernetes/hpa/popular-service-hpa.yaml delete mode 100644 code/kubernetes/hpa/search-service-hpa.yaml delete mode 100644 code/kubernetes/hpa/thread-service-hpa.yaml delete mode 100644 code/kubernetes/hpa/vote-service-hpa.yaml diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 2fa4494..57381f5 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -85,3 +85,22 @@ spec: port: 8080 initialDelaySeconds: 10 timeoutSeconds: 5 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: grpc-gateway-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: grpc-gateway + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/hpa/comment-service-hpa.yaml b/code/kubernetes/hpa/comment-service-hpa.yaml deleted file mode 100644 index 8826295..0000000 --- a/code/kubernetes/hpa/comment-service-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: comment-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: comment-service - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/community-service-hpa.yaml b/code/kubernetes/hpa/community-service-hpa.yaml deleted file mode 100644 index 22aca1d..0000000 --- a/code/kubernetes/hpa/community-service-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: community-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: community-service - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/grpc-gateway-hpa.yaml b/code/kubernetes/hpa/grpc-gateway-hpa.yaml deleted file mode 100644 index 483318e..0000000 --- a/code/kubernetes/hpa/grpc-gateway-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: grpc-gateway-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: grpc-gateway - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/popular-service-hpa.yaml b/code/kubernetes/hpa/popular-service-hpa.yaml deleted file mode 100644 index b08321b..0000000 --- a/code/kubernetes/hpa/popular-service-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: popular-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: popular-service - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/search-service-hpa.yaml b/code/kubernetes/hpa/search-service-hpa.yaml deleted file mode 100644 index adfb5d1..0000000 --- a/code/kubernetes/hpa/search-service-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: search-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: search-service - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/thread-service-hpa.yaml b/code/kubernetes/hpa/thread-service-hpa.yaml deleted file mode 100644 index 333d0c2..0000000 --- a/code/kubernetes/hpa/thread-service-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: thread-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: thread-service - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/hpa/vote-service-hpa.yaml b/code/kubernetes/hpa/vote-service-hpa.yaml deleted file mode 100644 index f273f07..0000000 --- a/code/kubernetes/hpa/vote-service-hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: vote-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: vote-service - minReplicas: 1 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 \ No newline at end of file diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 7d057f7..26d5748 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -55,3 +55,22 @@ spec: port: 50054 initialDelaySeconds: 6 timeoutSeconds: 4 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: comment-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: comment-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index 5e42b86..71219ef 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -55,3 +55,22 @@ spec: port: 50052 initialDelaySeconds: 6 timeoutSeconds: 4 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: community-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: community-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index 12621a9..9010829 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -59,3 +59,22 @@ spec: items: - key: gcs-key.json path: gcs-key.json +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: db-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: db-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index 257a2e2..a9c219d 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -55,3 +55,22 @@ spec: port: 50057 initialDelaySeconds: 6 timeoutSeconds: 4 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: popular-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: popular-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index a98cd31..d3e9c07 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -55,3 +55,22 @@ spec: port: 50056 initialDelaySeconds: 6 timeoutSeconds: 4 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: search-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: search-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index 6eadf1c..a3c1bd9 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -55,3 +55,22 @@ spec: port: 50053 initialDelaySeconds: 6 timeoutSeconds: 4 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: thread-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: thread-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index 0ea3a85..f5701ee 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -55,3 +55,22 @@ spec: port: 50055 initialDelaySeconds: 6 timeoutSeconds: 4 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: vote-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: vote-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 \ No newline at end of file From 78e399fbfc77fdde9cdcb3b8a146d003f9f047c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 20 May 2025 07:39:10 +0100 Subject: [PATCH 12/18] Increased resources values --- code/kubernetes/grpc-gateway/deployment.yaml | 8 ++++---- code/kubernetes/services/comment-service/deployment.yaml | 8 ++++---- .../kubernetes/services/community-service/deployment.yaml | 8 ++++---- code/kubernetes/services/db-service/deployment.yaml | 8 ++++---- code/kubernetes/services/popular-service/deployment.yaml | 8 ++++---- code/kubernetes/services/search-service/deployment.yaml | 8 ++++---- code/kubernetes/services/thread-service/deployment.yaml | 8 ++++---- code/kubernetes/services/vote-service/deployment.yaml | 8 ++++---- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 57381f5..5cb8167 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 8080 resources: requests: - cpu: "200m" - memory: "256Mi" + cpu: 30m + memory: 60Mi limits: - cpu: "500m" - memory: "512Mi" + cpu: 120m + memory: 240Mi env: - name: GRPC_GATEWAY_PORT valueFrom: diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 26d5748..691a5a8 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50054 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 20m + memory: 40Mi limits: - cpu: 50m - memory: 100Mi + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index 71219ef..d0cc0a6 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50052 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 20m + memory: 40Mi limits: - cpu: 50m - memory: 100Mi + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index 9010829..5c35074 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50051 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 30m + memory: 60Mi limits: - cpu: 100m - memory: 200Mi + cpu: 120m + memory: 240Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index a9c219d..adb7d9e 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50057 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 20m + memory: 40Mi limits: - cpu: 50m - memory: 100Mi + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index d3e9c07..1ac1d49 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50056 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 20m + memory: 40Mi limits: - cpu: 50m - memory: 100Mi + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index a3c1bd9..86fcbd9 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50053 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 20m + memory: 40Mi limits: - cpu: 50m - memory: 100Mi + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index f5701ee..97325cd 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50055 resources: requests: - cpu: 10m - memory: 20Mi + cpu: 20m + memory: 40Mi limits: - cpu: 50m - memory: 100Mi + cpu: 100m + memory: 200Mi env: - name: SERVICE_PORT valueFrom: From 0d85c8d1737d33054a9def9ca15d8a174436c995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Wed, 21 May 2025 00:00:31 +0100 Subject: [PATCH 13/18] Revert "Authentication & Authorization with Keycloak" --- code/docker-compose.yml | 21 -- code/grpc-gateway/main.go | 103 ++----- code/grpc-gateway/middleware/auth.go | 163 ----------- code/grpc-gateway/middleware/auth_routes.go | 274 ------------------ code/keycloak/realm-export.json | 53 ---- code/kubernetes/keycloak/configmap.yaml | 10 - code/kubernetes/keycloak/deployment.yaml | 63 ---- code/kubernetes/keycloak/realm-configmap.yaml | 46 --- code/kubernetes/keycloak/secrets.yaml | 11 - code/kubernetes/scripts/deploy.sh | 19 +- code/kubernetes/scripts/keycloak-ops.sh | 65 ----- code/kubernetes/traefik/ingress-routes.yaml | 54 ---- code/services/auth/auth.go | 120 -------- code/traefik/traefik.yml | 51 ---- 14 files changed, 28 insertions(+), 1025 deletions(-) delete mode 100644 code/grpc-gateway/middleware/auth.go delete mode 100644 code/grpc-gateway/middleware/auth_routes.go delete mode 100644 code/keycloak/realm-export.json delete mode 100644 code/kubernetes/keycloak/configmap.yaml delete mode 100644 code/kubernetes/keycloak/deployment.yaml delete mode 100644 code/kubernetes/keycloak/realm-configmap.yaml delete mode 100644 code/kubernetes/keycloak/secrets.yaml delete mode 100644 code/kubernetes/scripts/keycloak-ops.sh delete mode 100644 code/kubernetes/traefik/ingress-routes.yaml delete mode 100644 code/services/auth/auth.go diff --git a/code/docker-compose.yml b/code/docker-compose.yml index ba4b038..261f24f 100644 --- a/code/docker-compose.yml +++ b/code/docker-compose.yml @@ -194,27 +194,6 @@ services: networks: - threadit-network - keycloak: - image: quay.io/keycloak/keycloak:21.1 - container_name: keycloak - restart: always - command: - - start-dev - - --import-realm - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} - KC_HOSTNAME_STRICT: false - KC_HOSTNAME_STRICT_HTTPS: false - KC_HTTP_ENABLED: "true" - KC_PROXY: edge - volumes: - - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro - ports: - - "${KEYCLOAK_PORT}:8080" - networks: - - threadit-network - volumes: db_data: driver: local diff --git a/code/grpc-gateway/main.go b/code/grpc-gateway/main.go index e58e362..3f5bbfe 100644 --- a/code/grpc-gateway/main.go +++ b/code/grpc-gateway/main.go @@ -17,7 +17,6 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "threadit/grpc-gateway/middleware" "google.golang.org/protobuf/types/known/emptypb" ) @@ -109,24 +108,8 @@ func handleHealthCheck(w http.ResponseWriter, r *http.Request) { } func main() { - ctx := context.Background() - mux := runtime.NewServeMux() - - // Initialize auth handler - authHandler := middleware.NewAuthHandler( - os.Getenv("KEYCLOAK_URL"), - os.Getenv("KEYCLOAK_CLIENT_ID"), - os.Getenv("KEYCLOAK_CLIENT_SECRET"), - os.Getenv("KEYCLOAK_REALM"), - ) - - // Create a new ServeMux for both gRPC-Gateway and auth routes - httpMux := http.NewServeMux() + gwmux := runtime.NewServeMux() - // Register auth routes - authHandler.RegisterRoutes(httpMux) - - // gRPC dial options with message size configurations opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions( @@ -135,80 +118,48 @@ func main() { ), } - // Register gRPC-Gateway routes with auth middleware - httpMux.Handle("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Auth middleware for API routes - authMiddleware := middleware.NewAuthMiddleware(middleware.KeycloakConfig{ - Realm: os.Getenv("KEYCLOAK_REALM"), - ClientID: os.Getenv("KEYCLOAK_CLIENT_ID"), - ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"), - KeycloakURL: os.Getenv("KEYCLOAK_URL"), - }) - - authMiddleware.Handler(mux).ServeHTTP(w, r) - })) - - // Register service handlers - if err := registerServices(ctx, mux, opts); err != nil { - log.Fatalf("Failed to register services: %v", err) - } - - port := os.Getenv("GRPC_GATEWAY_PORT") - if port == "" { - log.Fatalf("missing GRPC_GATEWAY_PORT env var") + err := communitypb.RegisterCommunityServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - log.Printf("gRPC Gateway server listening on :%s", port) - if err := http.ListenAndServe(":"+port, httpMux); err != nil { - log.Fatalf("Failed to serve: %v", err) + err = threadpb.RegisterThreadServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } -} -func registerServices(ctx context.Context, mux *runtime.ServeMux, opts []grpc.DialOption) error { - // Register Community Service - if err := communitypb.RegisterCommunityServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register community service: %v", err) + err = commentpb.RegisterCommentServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - // Register Thread Service - if err := threadpb.RegisterThreadServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register thread service: %v", err) + err = votepb.RegisterVoteServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - // Register Comment Service - if err := commentpb.RegisterCommentServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register comment service: %v", err) + err = searchpb.RegisterSearchServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - // Register Vote Service - if err := votepb.RegisterVoteServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register vote service: %v", err) + err = popularpb.RegisterPopularServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } http.HandleFunc("/health", handleHealthCheck) + http.Handle("/", gwmux) - // Register Search Service - if err := searchpb.RegisterSearchServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register search service: %v", err) + port := os.Getenv("GRPC_GATEWAY_PORT") + if port == "" { + log.Fatalf("missing GRPC_GATEWAY_PORT env var") } - // Register Popular Service - if err := popularpb.RegisterPopularServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register popular service: %v", err) + log.Printf("gRPC Gateway server listening on :%s", port) + err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) + if err != nil { + log.Fatalf("Failed to start HTTP server: %v", err) } - - return nil } diff --git a/code/grpc-gateway/middleware/auth.go b/code/grpc-gateway/middleware/auth.go deleted file mode 100644 index 95607b1..0000000 --- a/code/grpc-gateway/middleware/auth.go +++ /dev/null @@ -1,163 +0,0 @@ -package middleware - -import ( - "context" - "net/http" - "strings" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "google.golang.org/grpc/metadata" - "your-module/code/services/auth" -) - -type AuthMiddleware struct { - keycloak *auth.KeycloakClient -} - -func NewAuthMiddleware(config auth.KeycloakConfig) (*AuthMiddleware, error) { - kc, err := auth.NewKeycloakClient(config) - if err != nil { - return nil, err - } - return &AuthMiddleware{keycloak: kc}, nil -} - -func (am *AuthMiddleware) Handler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip auth for public endpoints - if isPublicEndpoint(r.URL.Path, r.Method) { - next.ServeHTTP(w, r) - return - } - - // Extract token from Authorization header - token, err := auth.ExtractBearerToken(r.Header.Get("Authorization")) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Validate token - claims, err := am.keycloak.ValidateToken(r.Context(), token) - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - // Check required roles for protected endpoints - if !hasRequiredRole(r.URL.Path, claims) { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - - // Add user info to context - ctx := context.WithValue(r.Context(), "user_claims", claims) - - // Forward token to gRPC services - md := metadata.Pairs("authorization", "Bearer "+token) - ctx = metadata.NewOutgoingContext(ctx, md) - - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func isPublicEndpoint(path, method string) bool { - // Auth endpoints are always public - authPaths := []string{ - "/auth/login", - "/auth/register", - "/auth/logout", - } - for _, ap := range authPaths { - if path == ap { - return true - } - } - - // Only GET requests can be public for these paths - if method != http.MethodGet { - return false - } - - publicGetPaths := []string{ - "/communities", - "/threads", - "/comments", - "/search", - "/search/thread", - "/search/community", - "/popular/threads", - "/popular/comments", - } - - // Check exact matches for list endpoints - for _, pp := range publicGetPaths { - if path == pp { - return true - } - } - - // Check id based paths - idBasedPaths := []string{ - "/communities/", - "/threads/", - "/comments/", - } - - for _, pp := range idBasedPaths { - if strings.HasPrefix(path, pp) && path != pp { - return true - } - } - - return false -} - -func hasRequiredRole(path string, claims *auth.TokenClaims) bool { - roleRequirements := map[string]string{ - // Communities - "POST /communities": "user", - "PATCH /communities/": "moderator", - "DELETE /communities/": "moderator", - - // Threads - "POST /threads": "user", - "PATCH /threads/": "user", - "DELETE /threads/": "user", - - // Comment sdpoints - "POST /comments": "user", - "PATCH /comments/": "user", - "DELETE /comments/": "user", - - // Votes - "POST /votes/thread/": "user", - "POST /votes/comment/": "user", - - // Admin - "POST /admin/": "admin", - "PUT /admin/": "admin", - "DELETE /admin/": "admin", - } - - // Check each role requirement - for pathPattern, requiredRole := range roleRequirements { - parts := strings.SplitN(pathPattern, " ", 2) - method, pattern := parts[0], parts[1] - if strings.HasPrefix(path, pattern) { - return claims.RealmAccess.Roles != nil && contains(claims.RealmAccess.Roles, requiredRole) - } - } - - // If no specific role requirement, allow access - return true -} - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} \ No newline at end of file diff --git a/code/grpc-gateway/middleware/auth_routes.go b/code/grpc-gateway/middleware/auth_routes.go deleted file mode 100644 index 0c2799d..0000000 --- a/code/grpc-gateway/middleware/auth_routes.go +++ /dev/null @@ -1,274 +0,0 @@ -package middleware - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "regexp" - "strings" - "time" -) - -type AuthHandler struct { - keycloakURL string - clientID string - clientSecret string - realm string - adminClientID string -} - -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type RegisterRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` -} - -type ErrorResponse struct { - Error string `json:"error"` - Description string `json:"error_description,omitempty"` -} - -func NewAuthHandler(keycloakURL, clientID, clientSecret, realm string) *AuthHandler { - return &AuthHandler{ - keycloakURL: keycloakURL, - clientID: clientID, - clientSecret: clientSecret, - realm: realm, - adminClientID: "admin-cli", - } -} - -func (h *AuthHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("/auth/register", h.handleRegister) - mux.HandleFunc("/auth/login", h.handleLogin) - mux.HandleFunc("/auth/logout", h.handleLogout) -} - -func (h *AuthHandler) validateRegister(req *RegisterRequest) error { - // Username validation - if len(req.Username) < 3 || len(req.Username) > 30 { - return fmt.Errorf("username must be between 3 and 30 characters") - } - if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(req.Username) { - return fmt.Errorf("username can only contain letters, numbers, underscores, and hyphens") - } - - // Email validation - emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - if !emailRegex.MatchString(req.Email) { - return fmt.Errorf("invalid email format") - } - - // Password validation - if len(req.Password) < 8 { - return fmt.Errorf("password must be at least 8 characters long") - } - if !regexp.MustCompile(`[A-Z]`).MatchString(req.Password) { - return fmt.Errorf("password must contain at least one uppercase letter") - } - if !regexp.MustCompile(`[a-z]`).MatchString(req.Password) { - return fmt.Errorf("password must contain at least one lowercase letter") - } - if !regexp.MustCompile(`[0-9]`).MatchString(req.Password) { - return fmt.Errorf("password must contain at least one number") - } - - return nil -} - -func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req RegisterRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - // Validate registration data - if err := h.validateRegister(&req); err != nil { - h.sendError(w, err.Error(), http.StatusBadRequest) - return - } - - // Create user in Keycloak - keycloakURL := fmt.Sprintf("%s/auth/admin/realms/%s/users", h.keycloakURL, h.realm) - userData := map[string]interface{}{ - "username": req.Username, - "email": req.Email, - "enabled": true, - "credentials": []map[string]interface{}{ - { - "type": "password", - "value": req.Password, - "temporary": false, - }, - }, - } - - jsonData, err := json.Marshal(userData) - if err != nil { - h.sendError(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Get admin token - adminToken, err := h.getAdminToken() - if err != nil { - h.sendError(w, "Failed to authenticate with Keycloak", http.StatusInternalServerError) - return - } - - request, err := http.NewRequest(http.MethodPost, keycloakURL, strings.NewReader(string(jsonData))) - if err != nil { - h.sendError(w, "Internal server error", http.StatusInternalServerError) - return - } - - request.Header.Set("Content-Type", "application/json") - request.Header.Set("Authorization", "Bearer "+adminToken) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(request) - if err != nil { - h.sendError(w, "Failed to register user", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - body, _ := io.ReadAll(resp.Body) - h.sendError(w, fmt.Sprintf("Failed to register user: %s", string(body)), resp.StatusCode) - return - } - - // Auto login after registration - h.performLogin(w, req.Username, req.Password) -} - -func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - h.performLogin(w, req.Username, req.Password) -} - -func (h *AuthHandler) performLogin(w http.ResponseWriter, username, password string) { - tokenURL := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", h.keycloakURL, h.realm) - data := url.Values{} - data.Set("grant_type", "password") - data.Set("client_id", h.clientID) - data.Set("client_secret", h.clientSecret) - data.Set("username", username) - data.Set("password", password) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.PostForm(tokenURL, data) - if err != nil { - h.sendError(w, "Failed to login", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - h.sendError(w, "Invalid credentials", http.StatusUnauthorized) - return - } - - // Forward Keycloak response (tokens) to client - w.Header().Set("Content-Type", "application/json") - io.Copy(w, resp.Body) -} - -func (h *AuthHandler) handleLogout(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - token := r.Header.Get("Authorization") - if token == "" { - h.sendError(w, "No token provided", http.StatusBadRequest) - return - } - - logoutURL := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/logout", h.keycloakURL, h.realm) - request, err := http.NewRequest(http.MethodPost, logoutURL, nil) - if err != nil { - h.sendError(w, "Internal server error", http.StatusInternalServerError) - return - } - - request.Header.Set("Authorization", token) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(request) - if err != nil { - h.sendError(w, "Failed to logout", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - h.sendError(w, "Failed to logout", resp.StatusCode) - return - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"message": "Logged out successfully"}) -} - -func (h *AuthHandler) getAdminToken() (string, error) { - tokenURL := fmt.Sprintf("%s/auth/realms/master/protocol/openid-connect/token", h.keycloakURL) - data := url.Values{} - data.Set("grant_type", "client_credentials") - data.Set("client_id", h.adminClientID) - data.Set("client_secret", h.clientSecret) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.PostForm(tokenURL, data) - if err != nil { - return "", fmt.Errorf("failed to get admin token: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get admin token: status %d", resp.StatusCode) - } - - var result struct { - AccessToken string `json:"access_token"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", fmt.Errorf("failed to decode admin token response: %v", err) - } - - return result.AccessToken, nil -} - -func (h *AuthHandler) sendError(w http.ResponseWriter, message string, status int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(ErrorResponse{ - Error: http.StatusText(status), - Description: message, - }) -} \ No newline at end of file diff --git a/code/keycloak/realm-export.json b/code/keycloak/realm-export.json deleted file mode 100644 index 9452d7e..0000000 --- a/code/keycloak/realm-export.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "id": "threadit", - "realm": "threadit", - "enabled": true, - "roles": { - "realm": [ - { - "name": "user", - "description": "User role" - }, - { - "name": "moderator", - "description": "Community moderator role" - }, - { - "name": "admin", - "description": "Admin role" - } - ] - }, - "defaultRoles": ["user"], - "clients": [ - { - "clientId": "threadit-api", - "enabled": true, - "protocol": "openid-connect", - "publicClient": false, - "clientAuthenticatorType": "client-secret", - "secret": "${CLIENT_SECRET}", - "redirectUris": ["*"], - "webOrigins": ["*"], - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true - } - ], - "users": [ - { - "username": "admin", - "enabled": true, - "credentials": [ - { - "type": "password", - "value": "${ADMIN_PASSWORD}", - "temporary": false - } - ], - "realmRoles": ["admin"] - } - ] -} \ No newline at end of file diff --git a/code/kubernetes/keycloak/configmap.yaml b/code/kubernetes/keycloak/configmap.yaml deleted file mode 100644 index ea226ad..0000000 --- a/code/kubernetes/keycloak/configmap.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: keycloak-config - namespace: threadit -data: - KC_HOSTNAME_STRICT: "false" - KC_HOSTNAME_STRICT_HTTPS: "false" - KC_HTTP_ENABLED: "true" - KC_PROXY: "edge" \ No newline at end of file diff --git a/code/kubernetes/keycloak/deployment.yaml b/code/kubernetes/keycloak/deployment.yaml deleted file mode 100644 index d2b08bc..0000000 --- a/code/kubernetes/keycloak/deployment.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: keycloak - namespace: threadit - labels: - app: keycloak -spec: - ports: - - port: 8080 - targetPort: 8080 - protocol: TCP - selector: - app: keycloak ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: keycloak - namespace: threadit - labels: - app: keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: keycloak - template: - metadata: - labels: - app: keycloak - spec: - containers: - - name: keycloak - image: quay.io/keycloak/keycloak:21.1 - args: ["start-dev", "--import-realm"] - ports: - - containerPort: 8080 - envFrom: - - configMapRef: - name: keycloak-config - - secretRef: - name: keycloak-secrets - volumeMounts: - - name: realm-config - mountPath: /opt/keycloak/data/import - readOnly: true - readinessProbe: - httpGet: - path: /auth/realms/master - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /auth/realms/master - port: 8080 - initialDelaySeconds: 60 - periodSeconds: 15 - volumes: - - name: realm-config - configMap: - name: keycloak-realm-config \ No newline at end of file diff --git a/code/kubernetes/keycloak/realm-configmap.yaml b/code/kubernetes/keycloak/realm-configmap.yaml deleted file mode 100644 index 5a1a905..0000000 --- a/code/kubernetes/keycloak/realm-configmap.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: keycloak-realm-config - namespace: threadit -data: - realm.json: | - { - "id": "threadit", - "realm": "threadit", - "enabled": true, - "roles": { - "realm": [ - { - "name": "user", - "description": "User role" - }, - { - "name": "moderator", - "description": "Community moderator role" - }, - { - "name": "admin", - "description": "Admin role" - } - ] - }, - "defaultRoles": ["user"], - "clients": [ - { - "clientId": "threadit-api", - "enabled": true, - "protocol": "openid-connect", - "publicClient": false, - "clientAuthenticatorType": "client-secret", - "secret": "${CLIENT_SECRET}", - "redirectUris": ["*"], - "webOrigins": ["*"], - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true - } - ] - } \ No newline at end of file diff --git a/code/kubernetes/keycloak/secrets.yaml b/code/kubernetes/keycloak/secrets.yaml deleted file mode 100644 index fc4baa5..0000000 --- a/code/kubernetes/keycloak/secrets.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: keycloak-secrets - namespace: threadit -type: Opaque -data: - KC_DB_PASSWORD: a2V5Y2xvYWtfcGFzc3dvcmQ= - KEYCLOAK_ADMIN: YWRtaW4= - KEYCLOAK_ADMIN_PASSWORD: YWRtaW5fcGFzc3dvcmQ= - CLIENT_SECRET: eW91ci1jbGllbnQtc2VjcmV0 \ No newline at end of file diff --git a/code/kubernetes/scripts/deploy.sh b/code/kubernetes/scripts/deploy.sh index f0348cd..854279f 100644 --- a/code/kubernetes/scripts/deploy.sh +++ b/code/kubernetes/scripts/deploy.sh @@ -54,7 +54,6 @@ helm upgrade --install traefik traefik/traefik -n $CLUSTER_NAME -f traefik/value kubectl apply -n $CLUSTER_NAME -f traefik/cors.yaml kubectl apply -n $CLUSTER_NAME -f traefik/strip-prefix.yaml -kubectl apply -n $CLUSTER_NAME -f traefik/ingress-routes.yaml # Deploy threadit application kubectl create secret generic "bucket-secret" \ @@ -69,24 +68,8 @@ kubectl create secret generic "mongo-secret" \ kubectl apply -n $CLUSTER_NAME -f config.yaml kubectl apply -n $CLUSTER_NAME -f mongo/ -# Keycloak -echo "Deploying Keycloak..." -kubectl apply -n $CLUSTER_NAME -f keycloak/configmap.yaml -kubectl apply -n $CLUSTER_NAME -f keycloak/secrets.yaml -kubectl apply -n $CLUSTER_NAME -f keycloak/realm-configmap.yaml -kubectl apply -n $CLUSTER_NAME -f keycloak/deployment.yaml - -# Services for SERVICE in "${SERVICES[@]}"; do kubectl apply -n $CLUSTER_NAME -f services/"$SERVICE-service"/ done -# gRPC Gateway -kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ - -echo "Waiting for Keycloak to be ready..." -kubectl wait --for=condition=ready pod -l app=keycloak -n $CLUSTER_NAME --timeout=300s - -echo "Deployment complete!" -kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ - +kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ \ No newline at end of file diff --git a/code/kubernetes/scripts/keycloak-ops.sh b/code/kubernetes/scripts/keycloak-ops.sh deleted file mode 100644 index 170a5f4..0000000 --- a/code/kubernetes/scripts/keycloak-ops.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -set -e - -CLUSTER_NAME="threadit-cluster" - -function help() { - echo "Usage: $0 " - echo "Commands:" - echo " status - Check Keycloak status" - echo " logs - Show Keycloak logs" - echo " restart - Restart Keycloak deployment" - echo " reload - Reload realm configuration" - echo " port-forward - Start port forwarding to access Keycloak locally" -} - -function check_status() { - echo "Checking Keycloak status..." - kubectl get pods -n $CLUSTER_NAME -l app=keycloak -} - -function show_logs() { - echo "Fetching Keycloak logs..." - kubectl logs -n $CLUSTER_NAME -l app=keycloak --tail=100 -f -} - -function restart_keycloak() { - echo "Restarting Keycloak..." - kubectl rollout restart deployment/keycloak -n $CLUSTER_NAME - kubectl rollout status deployment/keycloak -n $CLUSTER_NAME -} - -function reload_realm() { - echo "Reloading realm configuration..." - # Delete the existing pod to force a reload of the realm config - kubectl delete pod -n $CLUSTER_NAME -l app=keycloak - echo "Waiting for new pod to be ready..." - kubectl wait --for=condition=ready pod -l app=keycloak -n $CLUSTER_NAME --timeout=300s -} - -function port_forward() { - echo "Starting port forward to Keycloak on localhost:8080..." - kubectl port-forward -n $CLUSTER_NAME svc/keycloak 8080:8080 -} - -case "$1" in - "status") - check_status - ;; - "logs") - show_logs - ;; - "restart") - restart_keycloak - ;; - "reload") - reload_realm - ;; - "port-forward") - port_forward - ;; - *) - help - exit 1 - ;; -esac \ No newline at end of file diff --git a/code/kubernetes/traefik/ingress-routes.yaml b/code/kubernetes/traefik/ingress-routes.yaml deleted file mode 100644 index 5620952..0000000 --- a/code/kubernetes/traefik/ingress-routes.yaml +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: traefik.containo.us/v1alpha1 -kind: Middleware -metadata: - name: cors-headers - namespace: threadit -spec: - headers: - accessControlAllowMethods: - - GET - - POST - - PUT - - DELETE - - PATCH - accessControlAllowHeaders: - - "Authorization" - - "Content-Type" - accessControlAllowOriginList: - - "*" - accessControlMaxAge: 100 - addVaryHeader: true ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: api - namespace: threadit -spec: - entryPoints: - - web - routes: - - match: PathPrefix(`/api/v1`) - kind: Rule - services: - - name: grpc-gateway - port: 8080 - middlewares: - - name: cors-headers ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: keycloak - namespace: threadit -spec: - entryPoints: - - web - routes: - - match: PathPrefix(`/auth`) - kind: Rule - services: - - name: keycloak - port: 8080 - middlewares: - - name: cors-headers \ No newline at end of file diff --git a/code/services/auth/auth.go b/code/services/auth/auth.go deleted file mode 100644 index faab083..0000000 --- a/code/services/auth/auth.go +++ /dev/null @@ -1,120 +0,0 @@ -package auth - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "time" - "github.com/golang-jwt/jwt/v4" -) - -var ( - ErrNoToken = errors.New("no token provided") - ErrInvalidToken = errors.New("invalid token") - ErrInsufficientRole = errors.New("insufficient role") -) - -type KeycloakConfig struct { - Realm string - ClientID string - ClientSecret string - KeycloakURL string -} - -type TokenClaims struct { - jwt.StandardClaims - RealmAccess struct { - Roles []string `json:"roles"` - } `json:"realm_access"` -} - -type KeycloakClient struct { - config KeycloakConfig - keys map[string]interface{} -} - -func NewKeycloakClient(config KeycloakConfig) (*KeycloakClient, error) { - kc := &KeycloakClient{ - config: config, - keys: make(map[string]interface{}), - } - if err := kc.fetchKeys(); err != nil { - return nil, err - } - return kc, nil -} - -func (kc *KeycloakClient) fetchKeys() error { - resp, err := http.Get(fmt.Sprintf("%s/realms/%s/protocol/openid-connect/certs", kc.config.KeycloakURL, kc.config.Realm)) - if err != nil { - return err - } - defer resp.Body.Close() - - var jwks struct { - Keys []struct { - Kid string `json:"kid"` - Kty string `json:"kty"` - Alg string `json:"alg"` - Use string `json:"use"` - N string `json:"n"` - E string `json:"e"` - } `json:"keys"` - } - - if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { - return err - } - - for _, key := range jwks.Keys { - kc.keys[key.Kid] = key - } - - return nil -} - -func (kc *KeycloakClient) ValidateToken(ctx context.Context, tokenString string) (*TokenClaims, error) { - token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { - if kid, ok := token.Header["kid"].(string); ok { - if key, exists := kc.keys[kid]; exists { - return key, nil - } - } - return nil, ErrInvalidToken - }) - - if err != nil { - return nil, fmt.Errorf("failed to parse token: %w", err) - } - - if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid { - return claims, nil - } - - return nil, ErrInvalidToken -} - -func (kc *KeycloakClient) HasRole(claims *TokenClaims, requiredRole string) bool { - for _, role := range claims.RealmAccess.Roles { - if role == requiredRole { - return true - } - } - return false -} - -func ExtractBearerToken(header string) (string, error) { - if header == "" { - return "", ErrNoToken - } - - parts := strings.Split(header, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - return "", ErrInvalidToken - } - - return parts[1], nil -} \ No newline at end of file diff --git a/code/traefik/traefik.yml b/code/traefik/traefik.yml index 3ac99dd..58366a4 100644 --- a/code/traefik/traefik.yml +++ b/code/traefik/traefik.yml @@ -3,63 +3,12 @@ global: sendAnonymousUsage: false api: - dashboard: true insecure: true entryPoints: web: address: ":80" - forwardedHeaders: - insecure: true providers: - docker: - exposedByDefault: false file: filename: "/etc/traefik/dynamic.yml" - -http: - middlewares: - cors-headers: - headers: - accessControlAllowMethods: - - GET - - POST - - PUT - - DELETE - - PATCH - accessControlAllowHeaders: - - "Authorization" - - "Content-Type" - accessControlAllowOriginList: - - "*" - accessControlMaxAge: 100 - addVaryHeader: true - - routers: - api: - rule: "PathPrefix(`/api/v1`)" - service: "grpc-gateway" - middlewares: - - "cors-headers" - entryPoints: - - "web" - - keycloak: - rule: "PathPrefix(`/auth`)" - service: "keycloak" - middlewares: - - "cors-headers" - entryPoints: - - "web" - - services: - grpc-gateway: - loadBalancer: - servers: - - url: "http://grpc-gateway:8080" - - keycloak: - loadBalancer: - servers: - - url: "http://keycloak:8080" From 816fb374b669ee0fa69579a95617ab2525fc4553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Wed, 21 May 2025 01:55:16 +0100 Subject: [PATCH 14/18] Some value changes --- code/kubernetes/grpc-gateway/deployment.yaml | 2 +- code/kubernetes/scripts/create-cluster.sh | 4 +- .../services/comment-service/deployment.yaml | 2 +- .../community-service/deployment.yaml | 2 +- .../services/db-service/deployment.yaml | 68 ++++++++++--------- .../services/popular-service/deployment.yaml | 2 +- .../services/search-service/deployment.yaml | 2 +- .../services/thread-service/deployment.yaml | 2 +- .../services/vote-service/deployment.yaml | 2 +- code/kubernetes/traefik/values.yaml | 8 +-- 10 files changed, 48 insertions(+), 46 deletions(-) diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 5cb8167..4a4d34b 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -103,4 +103,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/scripts/create-cluster.sh b/code/kubernetes/scripts/create-cluster.sh index e20ed38..6a6ba76 100644 --- a/code/kubernetes/scripts/create-cluster.sh +++ b/code/kubernetes/scripts/create-cluster.sh @@ -3,7 +3,7 @@ set -e PROJECT_ID="threadit-api" CLUSTER_NAME="threadit-cluster" -MACHINE_TYPE="e2-standard-4" +MACHINE_TYPE="e2-medium" ZONE="europe-west1-b" gcloud config set project $PROJECT_ID @@ -16,7 +16,7 @@ gcloud container clusters create $CLUSTER_NAME \ --machine-type=$MACHINE_TYPE \ --zone=$ZONE \ --disk-type=pd-standard \ - --disk-size=20 + --disk-size=25 gcloud container clusters get-credentials $CLUSTER_NAME --zone=$ZONE diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 691a5a8..7b4c8b8 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -73,4 +73,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index d0cc0a6..d43b57c 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -73,4 +73,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index 5c35074..09beca7 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -20,11 +20,11 @@ spec: - containerPort: 50051 resources: requests: - cpu: 30m - memory: 60Mi + cpu: 100m + memory: 200Mi limits: - cpu: 120m - memory: 240Mi + cpu: 200m + memory: 2Gi env: - name: SERVICE_PORT valueFrom: @@ -42,16 +42,17 @@ spec: - mountPath: /var/secret/gcp/ name: bucket-credentials readOnly: true - readinessProbe: - tcpSocket: - port: 50051 - initialDelaySeconds: 8 - timeoutSeconds: 5 - livenessProbe: - tcpSocket: - port: 50051 - initialDelaySeconds: 8 - timeoutSeconds: 5 +# TODO this might be causing this service to crash +# readinessProbe: +# tcpSocket: +# port: 50051 +# initialDelaySeconds: 8 +# timeoutSeconds: 5 +# livenessProbe: +# tcpSocket: +# port: 50051 +# initialDelaySeconds: 8 +# timeoutSeconds: 5 volumes: - name: bucket-credentials secret: @@ -59,22 +60,23 @@ spec: items: - key: gcs-key.json path: gcs-key.json ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: db-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: db-service - minReplicas: 1 - maxReplicas: 3 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 60 \ No newline at end of file +# TODO this might be causing this service to crash +#--- +#apiVersion: autoscaling/v2 +#kind: HorizontalPodAutoscaler +#metadata: +# name: db-service-hpa +#spec: +# scaleTargetRef: +# apiVersion: apps/v1 +# kind: Deployment +# name: db-service +# minReplicas: 1 +# maxReplicas: 3 +# metrics: +# - type: Resource +# resource: +# name: cpu +# target: +# type: Utilization +# averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index adb7d9e..84e6d57 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -73,4 +73,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index 1ac1d49..ad29cb3 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -73,4 +73,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index 86fcbd9..c8165ad 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -73,4 +73,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index 97325cd..dabffb4 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -73,4 +73,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 60 \ No newline at end of file + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/traefik/values.yaml b/code/kubernetes/traefik/values.yaml index 504a34c..766d276 100644 --- a/code/kubernetes/traefik/values.yaml +++ b/code/kubernetes/traefik/values.yaml @@ -4,11 +4,11 @@ replicaCount: 1 resources: requests: - cpu: 20m - memory: 40Mi + cpu: 30m + memory: 60Mi limits: - cpu: 100m - memory: 100Mi + cpu: 120m + memory: 240Mi # TODO: If needed, uncomment and configure HPA settings #autoscaling: From 1e7b25306bae870c7f7ec9eaafbcd98fb08ea934 Mon Sep 17 00:00:00 2001 From: manuelfdg Date: Tue, 20 May 2025 23:00:18 +0000 Subject: [PATCH 15/18] Add initial CI/CD --- code/.github/workflows/ci-cd.yaml | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 code/.github/workflows/ci-cd.yaml diff --git a/code/.github/workflows/ci-cd.yaml b/code/.github/workflows/ci-cd.yaml new file mode 100644 index 0000000..61ab159 --- /dev/null +++ b/code/.github/workflows/ci-cd.yaml @@ -0,0 +1,35 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + PROJECT_ID: threadit-api + CLUSTER_NAME: threadit-cluster + ZONE: europe-west1-b + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ env.PROJECT_ID }} + service_account_key: ${{ secrets.GCP_SA_KEY }} + export_default_credentials: true + + - name: Authenticate Docker to Google Cloud + run: gcloud auth configure-docker + + - name: Deploy all services to GKE + run: | + chmod +x code/kubernetes/scripts/deploy.sh + code/kubernetes/scripts/deploy.sh \ No newline at end of file From 2a14bf39d64b5978680efe2968de920c8e2a8816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 27 May 2025 01:33:59 +0100 Subject: [PATCH 16/18] Improved build and deploy workflow --- .github/workflows/main.yaml | 99 +++++++++++++++++++++++++++++++ code/.github/workflows/ci-cd.yaml | 35 ----------- 2 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/main.yaml delete mode 100644 code/.github/workflows/ci-cd.yaml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..103b0ea --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,99 @@ +name: Build and Deploy to GKE + +on: + push: + branches: + - main + +env: + PROJECT_ID: threadit-api + CLUSTER_NAME: threadit-cluster + ZONE: europe-west1-b + GCS_KEY: gcs-key + SERVICES: db community thread comment vote search popular + +jobs: + setup-build-publish-deploy: + name: Setup, Build, Publish, and Deploy + runs-on: ubuntu-latest + environment: production + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ env.PROJECT_ID }} + + - name: Set up GKE credentials + uses: google-github-actions/get-gke-credentials@v2 + with: + project_id: ${{ env.PROJECT_ID }} + cluster_name: ${{ env.CLUSTER_NAME }} + location: ${{ env.ZONE }} + + - name: Configure Docker for GCR + run: | + gcloud auth configure-docker --quiet + + - name: Build and push images to GCR + working-directory: code/kubernetes + run: | + for SERVICE in $SERVICES; do + docker build -t gcr.io/$PROJECT_ID/${SERVICE}-service:latest -f services/${SERVICE}-service/Dockerfile . + docker push gcr.io/$PROJECT_ID/${SERVICE}-service:latest + done + + docker build -t gcr.io/$PROJECT_ID/grpc-gateway:latest -f grpc-gateway/Dockerfile . + docker push gcr.io/$PROJECT_ID/grpc-gateway:latest + + - name: Deploy Traefik + working-directory: code/kubernetes + run: | + helm repo add traefik https://traefik.github.io/charts + helm repo update + helm upgrade --install traefik traefik/traefik -n $CLUSTER_NAME -f traefik/values.yaml + + kubectl apply -n $CLUSTER_NAME -f traefik/cors.yaml + kubectl apply -n $CLUSTER_NAME -f traefik/strip-prefix.yaml + + - name: Create Kubernetes secrets + run: | + BUCKET_SECRET=$(gcloud secrets versions access latest --secret=$GCS_KEY) + MONGO_USER=$(gcloud secrets versions access latest --secret="mongo-user") + MONGO_PASS=$(gcloud secrets versions access latest --secret="mongo-pass") + + kubectl create secret generic "bucket-secret" \ + --from-literal="$GCS_KEY.json=$BUCKET_SECRET" \ + -n $CLUSTER_NAME --dry-run=client -o yaml | kubectl apply -f - + + kubectl create secret generic "mongo-secret" \ + --from-literal="MONGO_INITDB_ROOT_USERNAME=$MONGO_USER" \ + --from-literal="MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASS" \ + -n $CLUSTER_NAME --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy configuration and Mongo + working-directory: code/kubernetes + run: | + kubectl apply -n $CLUSTER_NAME -f config.yaml + kubectl apply -n $CLUSTER_NAME -f mongo/ + + - name: Deploy services + working-directory: code/kubernetes + run: | + for SERVICE in $SERVICES; do + kubectl apply -n $CLUSTER_NAME -f services/${SERVICE}-service/ + done + + kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ \ No newline at end of file diff --git a/code/.github/workflows/ci-cd.yaml b/code/.github/workflows/ci-cd.yaml deleted file mode 100644 index 61ab159..0000000 --- a/code/.github/workflows/ci-cd.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: CI/CD Pipeline - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -env: - PROJECT_ID: threadit-api - CLUSTER_NAME: threadit-cluster - ZONE: europe-west1-b - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@v2 - with: - project_id: ${{ env.PROJECT_ID }} - service_account_key: ${{ secrets.GCP_SA_KEY }} - export_default_credentials: true - - - name: Authenticate Docker to Google Cloud - run: gcloud auth configure-docker - - - name: Deploy all services to GKE - run: | - chmod +x code/kubernetes/scripts/deploy.sh - code/kubernetes/scripts/deploy.sh \ No newline at end of file From afd2dbc333643a043442aa24d956b07217e200f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 27 May 2025 23:27:19 +0100 Subject: [PATCH 17/18] Resources configuration done. Fixed db-service probe race condition --- code/kubernetes/grpc-gateway/deployment.yaml | 6 +- code/kubernetes/mongo/deployment.yaml | 6 +- code/kubernetes/scripts/README.md | 1 + code/kubernetes/scripts/cluster-info.sh | 23 +------ code/kubernetes/scripts/create-cluster.sh | 6 +- .../services/comment-service/deployment.yaml | 12 ++-- .../community-service/deployment.yaml | 12 ++-- .../services/db-service/deployment.yaml | 69 ++++++++++--------- .../services/popular-service/deployment.yaml | 12 ++-- .../services/search-service/deployment.yaml | 12 ++-- .../services/thread-service/deployment.yaml | 12 ++-- .../services/vote-service/deployment.yaml | 12 ++-- code/kubernetes/traefik/values.yaml | 4 +- 13 files changed, 85 insertions(+), 102 deletions(-) diff --git a/code/kubernetes/grpc-gateway/deployment.yaml b/code/kubernetes/grpc-gateway/deployment.yaml index 4a4d34b..529ff7f 100644 --- a/code/kubernetes/grpc-gateway/deployment.yaml +++ b/code/kubernetes/grpc-gateway/deployment.yaml @@ -20,8 +20,8 @@ spec: - containerPort: 8080 resources: requests: - cpu: 30m - memory: 60Mi + cpu: 40m + memory: 80Mi limits: cpu: 120m memory: 240Mi @@ -83,7 +83,7 @@ spec: httpGet: path: /health port: 8080 - initialDelaySeconds: 10 + initialDelaySeconds: 30 timeoutSeconds: 5 --- apiVersion: autoscaling/v2 diff --git a/code/kubernetes/mongo/deployment.yaml b/code/kubernetes/mongo/deployment.yaml index a68ce41..3810484 100644 --- a/code/kubernetes/mongo/deployment.yaml +++ b/code/kubernetes/mongo/deployment.yaml @@ -19,7 +19,7 @@ spec: - containerPort: 27017 resources: requests: - cpu: 150m + cpu: 200m memory: 700Mi limits: cpu: 500m @@ -49,7 +49,7 @@ spec: - mongosh - --eval - "db.adminCommand('ping')" - initialDelaySeconds: 15 + initialDelaySeconds: 60 timeoutSeconds: 5 readinessProbe: exec: @@ -57,7 +57,7 @@ spec: - mongosh - --eval - "db.adminCommand('ping')" - initialDelaySeconds: 15 + initialDelaySeconds: 20 timeoutSeconds: 5 volumes: - name: mongodb-data diff --git a/code/kubernetes/scripts/README.md b/code/kubernetes/scripts/README.md index 0716185..c44a3d7 100644 --- a/code/kubernetes/scripts/README.md +++ b/code/kubernetes/scripts/README.md @@ -45,6 +45,7 @@ $ ./cluster-info.sh - `--deployments` Shows deployment configurations and statuses for the namespace. - `--resources-pods` Displays real-time CPU and memory usage metrics for each pod. - `--resources-nodes` Displays real-time CPU and memory usage metrics for each node in the cluster. +- `--hpa` Shows all Horizontal Pod Autoscalers in the namespace, including current and target scaling metrics. - `--all` Runs all of the above commands to display full cluster info. ### 4. Delete Cluster diff --git a/code/kubernetes/scripts/cluster-info.sh b/code/kubernetes/scripts/cluster-info.sh index 6301967..5d56aa5 100644 --- a/code/kubernetes/scripts/cluster-info.sh +++ b/code/kubernetes/scripts/cluster-info.sh @@ -12,8 +12,6 @@ SHOW_DEPLOYMENTS=false SHOW_RESOURCES_PODS=false SHOW_RESOURCES_NODES=false SHOW_HPA=false -SHOW_DETAILED_RESOURCES=false -SHOW_EVENTS=false if [[ $# -eq 0 ]]; then SHOW_NAMESPACES=true @@ -23,8 +21,6 @@ if [[ $# -eq 0 ]]; then SHOW_RESOURCES_PODS=true SHOW_RESOURCES_NODES=true SHOW_HPA=true - SHOW_DETAILED_RESOURCES=true - SHOW_EVENTS=true fi # Parse flags @@ -37,8 +33,6 @@ while [[ "$#" -gt 0 ]]; do --resources-pods) SHOW_RESOURCES_PODS=true ;; --resources-nodes) SHOW_RESOURCES_NODES=true ;; --hpa) SHOW_HPA=true ;; - --detailed-resources) SHOW_DETAILED_RESOURCES=true ;; - --events) SHOW_EVENTS=true ;; --all) SHOW_NAMESPACES=true SHOW_PODS=true @@ -47,8 +41,6 @@ while [[ "$#" -gt 0 ]]; do SHOW_RESOURCES_PODS=true SHOW_RESOURCES_NODES=true SHOW_HPA=true - SHOW_DETAILED_RESOURCES=true - SHOW_EVENTS=true ;; *) echo "āŒ Unknown flag: $1"; exit 1 ;; esac @@ -65,17 +57,4 @@ $SHOW_SERVICES && echo -e "\nšŸ” Services:" && kubectl get svc -n $CLUSTER_NAME $SHOW_DEPLOYMENTS && echo -e "\nšŸ“‚ Deployments:" && kubectl get deployments -n $CLUSTER_NAME $SHOW_RESOURCES_PODS && echo -e "\nšŸ“Š Resource Usage (Pods):" && kubectl top pods -n $CLUSTER_NAME $SHOW_RESOURCES_NODES && echo -e "\nšŸ–„ļø Resource Usage (Nodes):" && kubectl top nodes - -# New sections for monitoring HPAs and resource limits -$SHOW_HPA && echo -e "\nāš–ļø Horizontal Pod Autoscalers:" && kubectl get hpa -n $CLUSTER_NAME - -if $SHOW_DETAILED_RESOURCES; then - echo -e "\nšŸ”Ž Detailed Resource Limits and Requests:" - echo "-------------------------------------------" - for pod in $(kubectl get pods -n $CLUSTER_NAME -o=name); do - echo -e "\nšŸ“Œ $pod" - kubectl describe $pod -n $CLUSTER_NAME | grep -A8 "Limits:" | grep -v "Node:" - done -fi - -$SHOW_EVENTS && echo -e "\nšŸ“œ Recent Events (including scaling):" && kubectl get events -n $CLUSTER_NAME --sort-by='.lastTimestamp' | grep -E '(HorizontalPodAutoscaler|scale|Scaled)' \ No newline at end of file +$SHOW_HPA && echo -e "\nšŸ“Œ Horizontal Pod Autoscalers:" && kubectl get hpa -n $CLUSTER_NAME \ No newline at end of file diff --git a/code/kubernetes/scripts/create-cluster.sh b/code/kubernetes/scripts/create-cluster.sh index 6a6ba76..a5b0b19 100644 --- a/code/kubernetes/scripts/create-cluster.sh +++ b/code/kubernetes/scripts/create-cluster.sh @@ -3,7 +3,7 @@ set -e PROJECT_ID="threadit-api" CLUSTER_NAME="threadit-cluster" -MACHINE_TYPE="e2-medium" +MACHINE_TYPE="e2-standard-2" ZONE="europe-west1-b" gcloud config set project $PROJECT_ID @@ -11,8 +11,8 @@ gcloud config set project $PROJECT_ID gcloud container clusters create $CLUSTER_NAME \ --num-nodes=3 \ --enable-autoscaling \ - --min-nodes=0 \ - --max-nodes=4 \ + --min-nodes=1 \ + --max-nodes=5 \ --machine-type=$MACHINE_TYPE \ --zone=$ZONE \ --disk-type=pd-standard \ diff --git a/code/kubernetes/services/comment-service/deployment.yaml b/code/kubernetes/services/comment-service/deployment.yaml index 7b4c8b8..9bd770e 100644 --- a/code/kubernetes/services/comment-service/deployment.yaml +++ b/code/kubernetes/services/comment-service/deployment.yaml @@ -23,8 +23,8 @@ spec: cpu: 20m memory: 40Mi limits: - cpu: 100m - memory: 200Mi + cpu: 60m + memory: 120Mi env: - name: SERVICE_PORT valueFrom: @@ -48,13 +48,13 @@ spec: readinessProbe: tcpSocket: port: 50054 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 5 + timeoutSeconds: 3 livenessProbe: tcpSocket: port: 50054 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 15 + timeoutSeconds: 3 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/code/kubernetes/services/community-service/deployment.yaml b/code/kubernetes/services/community-service/deployment.yaml index d43b57c..598daed 100644 --- a/code/kubernetes/services/community-service/deployment.yaml +++ b/code/kubernetes/services/community-service/deployment.yaml @@ -23,8 +23,8 @@ spec: cpu: 20m memory: 40Mi limits: - cpu: 100m - memory: 200Mi + cpu: 60m + memory: 120Mi env: - name: SERVICE_PORT valueFrom: @@ -48,13 +48,13 @@ spec: readinessProbe: tcpSocket: port: 50052 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 5 + timeoutSeconds: 3 livenessProbe: tcpSocket: port: 50052 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 15 + timeoutSeconds: 3 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/code/kubernetes/services/db-service/deployment.yaml b/code/kubernetes/services/db-service/deployment.yaml index 09beca7..cbf045f 100644 --- a/code/kubernetes/services/db-service/deployment.yaml +++ b/code/kubernetes/services/db-service/deployment.yaml @@ -21,9 +21,9 @@ spec: resources: requests: cpu: 100m - memory: 200Mi + memory: 250Mi limits: - cpu: 200m + cpu: 300m memory: 2Gi env: - name: SERVICE_PORT @@ -42,17 +42,21 @@ spec: - mountPath: /var/secret/gcp/ name: bucket-credentials readOnly: true -# TODO this might be causing this service to crash -# readinessProbe: -# tcpSocket: -# port: 50051 -# initialDelaySeconds: 8 -# timeoutSeconds: 5 -# livenessProbe: -# tcpSocket: -# port: 50051 -# initialDelaySeconds: 8 -# timeoutSeconds: 5 + livenessProbe: + tcpSocket: + port: 50051 + initialDelaySeconds: 60 + timeoutSeconds: 4 + readinessProbe: + tcpSocket: + port: 50051 + initialDelaySeconds: 30 + timeoutSeconds: 4 + startupProbe: + tcpSocket: + port: 50051 + periodSeconds: 2 + failureThreshold: 90 volumes: - name: bucket-credentials secret: @@ -60,23 +64,22 @@ spec: items: - key: gcs-key.json path: gcs-key.json -# TODO this might be causing this service to crash -#--- -#apiVersion: autoscaling/v2 -#kind: HorizontalPodAutoscaler -#metadata: -# name: db-service-hpa -#spec: -# scaleTargetRef: -# apiVersion: apps/v1 -# kind: Deployment -# name: db-service -# minReplicas: 1 -# maxReplicas: 3 -# metrics: -# - type: Resource -# resource: -# name: cpu -# target: -# type: Utilization -# averageUtilization: 80 \ No newline at end of file +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: db-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: db-service + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 \ No newline at end of file diff --git a/code/kubernetes/services/popular-service/deployment.yaml b/code/kubernetes/services/popular-service/deployment.yaml index 84e6d57..e6daa04 100644 --- a/code/kubernetes/services/popular-service/deployment.yaml +++ b/code/kubernetes/services/popular-service/deployment.yaml @@ -23,8 +23,8 @@ spec: cpu: 20m memory: 40Mi limits: - cpu: 100m - memory: 200Mi + cpu: 60m + memory: 120Mi env: - name: SERVICE_PORT valueFrom: @@ -48,13 +48,13 @@ spec: readinessProbe: tcpSocket: port: 50057 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 5 + timeoutSeconds: 3 livenessProbe: tcpSocket: port: 50057 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 15 + timeoutSeconds: 3 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/code/kubernetes/services/search-service/deployment.yaml b/code/kubernetes/services/search-service/deployment.yaml index ad29cb3..e51dae7 100644 --- a/code/kubernetes/services/search-service/deployment.yaml +++ b/code/kubernetes/services/search-service/deployment.yaml @@ -23,8 +23,8 @@ spec: cpu: 20m memory: 40Mi limits: - cpu: 100m - memory: 200Mi + cpu: 60m + memory: 120Mi env: - name: SERVICE_PORT valueFrom: @@ -48,13 +48,13 @@ spec: readinessProbe: tcpSocket: port: 50056 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 5 + timeoutSeconds: 3 livenessProbe: tcpSocket: port: 50056 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 15 + timeoutSeconds: 3 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/code/kubernetes/services/thread-service/deployment.yaml b/code/kubernetes/services/thread-service/deployment.yaml index c8165ad..11b9ee9 100644 --- a/code/kubernetes/services/thread-service/deployment.yaml +++ b/code/kubernetes/services/thread-service/deployment.yaml @@ -23,8 +23,8 @@ spec: cpu: 20m memory: 40Mi limits: - cpu: 100m - memory: 200Mi + cpu: 60m + memory: 120Mi env: - name: SERVICE_PORT valueFrom: @@ -48,13 +48,13 @@ spec: readinessProbe: tcpSocket: port: 50053 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 5 + timeoutSeconds: 3 livenessProbe: tcpSocket: port: 50053 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 15 + timeoutSeconds: 3 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/code/kubernetes/services/vote-service/deployment.yaml b/code/kubernetes/services/vote-service/deployment.yaml index dabffb4..44b329e 100644 --- a/code/kubernetes/services/vote-service/deployment.yaml +++ b/code/kubernetes/services/vote-service/deployment.yaml @@ -23,8 +23,8 @@ spec: cpu: 20m memory: 40Mi limits: - cpu: 100m - memory: 200Mi + cpu: 60m + memory: 120Mi env: - name: SERVICE_PORT valueFrom: @@ -48,13 +48,13 @@ spec: readinessProbe: tcpSocket: port: 50055 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 5 + timeoutSeconds: 3 livenessProbe: tcpSocket: port: 50055 - initialDelaySeconds: 6 - timeoutSeconds: 4 + initialDelaySeconds: 15 + timeoutSeconds: 3 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/code/kubernetes/traefik/values.yaml b/code/kubernetes/traefik/values.yaml index 766d276..56f8925 100644 --- a/code/kubernetes/traefik/values.yaml +++ b/code/kubernetes/traefik/values.yaml @@ -4,8 +4,8 @@ replicaCount: 1 resources: requests: - cpu: 30m - memory: 60Mi + cpu: 40m + memory: 80Mi limits: cpu: 120m memory: 240Mi From fb4dd0fde2f986ec165e169f3db0fb08e9636694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Tue, 27 May 2025 23:41:23 +0100 Subject: [PATCH 18/18] Added workflow safety when cluster does not exist --- .github/workflows/main.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 103b0ea..77c21f7 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -36,13 +36,30 @@ jobs: with: project_id: ${{ env.PROJECT_ID }} + - name: Check if GKE cluster exists + id: check-cluster + run: | + if gcloud container clusters describe $CLUSTER_NAME --zone $ZONE --project $PROJECT_ID; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "Cluster does not exist" + echo "exists=false" >> $GITHUB_OUTPUT + fi + - name: Set up GKE credentials + if: steps.check-cluster.outputs.exists == 'true' uses: google-github-actions/get-gke-credentials@v2 with: project_id: ${{ env.PROJECT_ID }} cluster_name: ${{ env.CLUSTER_NAME }} location: ${{ env.ZONE }} + - name: Cluster not created. Skip deployment + if: steps.check-cluster.outputs.exists == 'false' + run: | + echo "Cluster doesn't exist — skipping deployment." + exit 0 + - name: Configure Docker for GCR run: | gcloud auth configure-docker --quiet