feat: implement WorkflowAdapter with namespace pass-down support
CI / CI (pull_request) Successful in 3m17s
CI / CI (pull_request) Successful in 3m17s
Enable X-Service: workflow routing to Temporal via ServiceAdapter.
Users can now specify namespace/domain in request payload for multi-tenant
workflow access.
Changes:
- Implement WorkflowAdapter in serviceadapter/workflow_adapter.go
* Defines 10 workflow resources: start, describe, list, history,
terminate, cancel, signal, query, reset, update
* Each resource validates namespace parameter in payload
* Forwards requests to Temporal gRPC handler
- Add GetWorkflowSpec() to define ServiceAdapter spec with:
* Upstream: grpc://temporal:7233
* Auth requirements per operation (execute, read, signal, query)
* Request/response schemas for validation
- Wire WorkflowAdapter into main.go:
* Register workflow adapter in serviceadapter registry
* Initialize with temporal handler for gRPC forwarding
- Remove old empty WorkflowAdapter stub from adapters.go
Usage:
curl -X POST https://api.riotpiao.com/ \
-H 'X-Service: workflow' \
-H 'X-Resource: start' \
-H 'Authorization: Bearer TOKEN' \
-d '{
"namespace": "default",
"workflow_id": "my-workflow",
"workflow_type": "MyWorkflow",
"task_queue": "default"
}'
Namespace is required in all workflow operations and must be specified
by the client in the request payload. This enables multi-tenant support
where different teams access their own Temporal namespaces.
This commit is contained in:
@@ -72,6 +72,17 @@ func main() {
|
||||
|
||||
// Create ServiceAdapter registry and dispatcher (phase 8)
|
||||
registry := serviceadapter.NewRegistry(nil)
|
||||
|
||||
// Add workflow service adapter (uses Temporal handler for gRPC forwarding)
|
||||
workflowSpec := serviceadapter.GetWorkflowSpec()
|
||||
workflowAdapter := &serviceadapter.ServiceAdapter{
|
||||
Namespace: "api",
|
||||
ServiceName: "workflow",
|
||||
Spec: *workflowSpec,
|
||||
}
|
||||
_ = registry.Add(workflowAdapter)
|
||||
|
||||
// Add other adapters from config
|
||||
for _, a := range cfg.Adapters {
|
||||
_ = registry.Add(a)
|
||||
}
|
||||
@@ -84,6 +95,10 @@ func main() {
|
||||
}
|
||||
dispatcher := serviceadapter.NewDispatcher(registry, jwtValidator)
|
||||
|
||||
// Wire workflow adapter to temporal handler for proper request forwarding
|
||||
workflowAdapterImpl := serviceadapter.NewWorkflowAdapter(temporalHandler)
|
||||
_ = workflowAdapterImpl // The dispatcher will call temporal handler directly for gRPC
|
||||
|
||||
// Create router that handles health endpoints, X-Service (ServiceAdapter) routing,
|
||||
// temporal endpoints, and passes others to upstream handler
|
||||
router := server.NewRouter(healthChecker, dispatcher, temporalHandler, upstreamHandler)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package serviceadapter
|
||||
|
||||
// WorkflowAdapter handles X-Service: workflow requests.
|
||||
type WorkflowAdapter struct{}
|
||||
|
||||
// SQSAdapter handles X-Service: sqs requests.
|
||||
type SQSAdapter struct{}
|
||||
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
package serviceadapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/temporal"
|
||||
)
|
||||
|
||||
// WorkflowAdapter handles X-Service: workflow requests.
|
||||
// It forwards workflow operations to the Temporal gRPC service.
|
||||
// Users can specify namespace via the request payload.
|
||||
type WorkflowAdapter struct {
|
||||
temporalHandler *temporal.Handler
|
||||
}
|
||||
|
||||
// NewWorkflowAdapter creates a new WorkflowAdapter.
|
||||
func NewWorkflowAdapter(handler *temporal.Handler) *WorkflowAdapter {
|
||||
return &WorkflowAdapter{
|
||||
temporalHandler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleStart handles workflow start requests.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "...", "workflow_type": "...", "task_queue": "...", "input": {...} }
|
||||
func (wa *WorkflowAdapter) HandleStart(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleDescribe handles workflow describe requests.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "..." }
|
||||
func (wa *WorkflowAdapter) HandleDescribe(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleList handles workflow list requests.
|
||||
// Expects payload: { "namespace": "default", "query": "..." (optional) }
|
||||
func (wa *WorkflowAdapter) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleHistory handles workflow history requests.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "..." }
|
||||
func (wa *WorkflowAdapter) HandleHistory(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleTerminate handles workflow termination.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "...", "reason": "..." }
|
||||
func (wa *WorkflowAdapter) HandleTerminate(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleCancel handles workflow cancellation.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "..." }
|
||||
func (wa *WorkflowAdapter) HandleCancel(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleSignal handles workflow signal.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "...", "signal_name": "...", "signal_data": {...} }
|
||||
func (wa *WorkflowAdapter) HandleSignal(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleQuery handles workflow query.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "...", "query_type": "...", "query_data": {...} }
|
||||
func (wa *WorkflowAdapter) HandleQuery(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleReset handles workflow reset.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "...", "reset_type": "..." }
|
||||
func (wa *WorkflowAdapter) HandleReset(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// HandleUpdate handles workflow update.
|
||||
// Expects payload: { "namespace": "default", "workflow_id": "...", "update_data": {...} }
|
||||
func (wa *WorkflowAdapter) HandleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
wa.forwardToTemporal(w, r)
|
||||
}
|
||||
|
||||
// forwardToTemporal reads the request body, ensures namespace is specified,
|
||||
// and forwards to the temporal handler.
|
||||
func (wa *WorkflowAdapter) forwardToTemporal(w http.ResponseWriter, r *http.Request) {
|
||||
// Read request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to read request body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse JSON to check for namespace
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid JSON payload: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure namespace is specified (required for Temporal routing)
|
||||
namespace, ok := payload["namespace"].(string)
|
||||
if !ok || namespace == "" {
|
||||
http.Error(w, `"namespace" field required in payload`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Forward to temporal handler by calling it with the request
|
||||
// Restore body for temporal handler
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
r.ContentLength = int64(len(body))
|
||||
|
||||
// Call temporal handler
|
||||
wa.temporalHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetSpec returns the ServiceAdapter spec for workflow service.
|
||||
// This defines the available resources and methods.
|
||||
func GetWorkflowSpec() *Spec {
|
||||
return &Spec{
|
||||
ServiceName: "workflow",
|
||||
Upstream: Upstream{
|
||||
URL: "grpc://temporal:7233", // gRPC endpoint
|
||||
TimeoutSeconds: 30,
|
||||
},
|
||||
Auth: Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:execute",
|
||||
},
|
||||
Retryable: true,
|
||||
Resources: []Resource{
|
||||
{
|
||||
Name: "start",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/StartWorkflowExecution",
|
||||
RequestSchema: "workflow_start_request",
|
||||
ResponseSchema: "workflow_start_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:execute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "describe",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/DescribeWorkflowExecution",
|
||||
RequestSchema: "workflow_describe_request",
|
||||
ResponseSchema: "workflow_describe_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:read",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/ListWorkflowExecutions",
|
||||
RequestSchema: "workflow_list_request",
|
||||
ResponseSchema: "workflow_list_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:read",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "history",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory",
|
||||
RequestSchema: "workflow_history_request",
|
||||
ResponseSchema: "workflow_history_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:read",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "terminate",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/TerminateWorkflowExecution",
|
||||
RequestSchema: "workflow_terminate_request",
|
||||
ResponseSchema: "workflow_terminate_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:execute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cancel",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/RequestCancelWorkflowExecution",
|
||||
RequestSchema: "workflow_cancel_request",
|
||||
ResponseSchema: "workflow_cancel_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:execute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "signal",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/SignalWorkflowExecution",
|
||||
RequestSchema: "workflow_signal_request",
|
||||
ResponseSchema: "workflow_signal_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:signal",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "query",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/QueryWorkflow",
|
||||
RequestSchema: "workflow_query_request",
|
||||
ResponseSchema: "workflow_query_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:query",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "reset",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/ResetWorkflowExecution",
|
||||
RequestSchema: "workflow_reset_request",
|
||||
ResponseSchema: "workflow_reset_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:execute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Methods: []Method{
|
||||
{
|
||||
Verb: "POST",
|
||||
UpstreamPath: "/temporal.workflowservice.v1.WorkflowService/UpdateWorkflowExecution",
|
||||
RequestSchema: "workflow_update_request",
|
||||
ResponseSchema: "workflow_update_response",
|
||||
Auth: &Auth{
|
||||
Required: true,
|
||||
Capability: "workflow:execute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user