e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
384 lines
10 KiB
Go
384 lines
10 KiB
Go
// Copyright 2025 Alibaba Group Holding Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/klog/v2"
|
|
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/config"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/manager"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/types"
|
|
api "github.com/alibaba/OpenSandbox/sandbox-k8s/pkg/task-executor"
|
|
)
|
|
|
|
// ErrorResponse represents a standard error response
|
|
type ErrorResponse struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type Handler struct {
|
|
manager manager.TaskManager
|
|
config *config.Config
|
|
}
|
|
|
|
func NewHandler(mgr manager.TaskManager, cfg *config.Config) *Handler {
|
|
if mgr == nil {
|
|
klog.Warning("TaskManager is nil, handler may not work properly")
|
|
}
|
|
if cfg == nil {
|
|
klog.Warning("Config is nil, handler may not work properly")
|
|
}
|
|
return &Handler{
|
|
manager: mgr,
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
func (h *Handler) CreateTask(w http.ResponseWriter, r *http.Request) {
|
|
if h.manager == nil {
|
|
writeError(w, http.StatusInternalServerError, "task manager not initialized")
|
|
return
|
|
}
|
|
|
|
var apiTask api.Task
|
|
if err := json.NewDecoder(r.Body).Decode(&apiTask); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request body: %v", err))
|
|
return
|
|
}
|
|
|
|
if apiTask.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "task name is required")
|
|
return
|
|
}
|
|
|
|
task := h.convertAPIToInternalTask(&apiTask)
|
|
if task == nil {
|
|
writeError(w, http.StatusBadRequest, "failed to convert task")
|
|
return
|
|
}
|
|
|
|
created, err := h.manager.Create(r.Context(), task)
|
|
if err != nil {
|
|
klog.ErrorS(err, "failed to create task", "name", apiTask.Name)
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create task: %v", err))
|
|
return
|
|
}
|
|
|
|
response := convertInternalToAPITask(created)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
klog.InfoS("task created via API", "name", apiTask.Name)
|
|
}
|
|
|
|
func (h *Handler) SyncTasks(w http.ResponseWriter, r *http.Request) {
|
|
if h.manager == nil {
|
|
writeError(w, http.StatusInternalServerError, "task manager not initialized")
|
|
return
|
|
}
|
|
|
|
var apiTasks []api.Task
|
|
if err := json.NewDecoder(r.Body).Decode(&apiTasks); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request body: %v", err))
|
|
return
|
|
}
|
|
|
|
desired := make([]*types.Task, 0, len(apiTasks))
|
|
for i := range apiTasks {
|
|
if apiTasks[i].Name == "" {
|
|
continue
|
|
}
|
|
task := h.convertAPIToInternalTask(&apiTasks[i])
|
|
if task != nil {
|
|
desired = append(desired, task)
|
|
}
|
|
}
|
|
|
|
current, err := h.manager.Sync(r.Context(), desired)
|
|
if err != nil {
|
|
klog.ErrorS(err, "failed to sync tasks")
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to sync tasks: %v", err))
|
|
return
|
|
}
|
|
|
|
response := make([]api.Task, 0, len(current))
|
|
for _, task := range current {
|
|
if task != nil {
|
|
response = append(response, *convertInternalToAPITask(task))
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
klog.V(1).InfoS("tasks synced via API", "count", len(response))
|
|
}
|
|
|
|
func (h *Handler) GetTask(w http.ResponseWriter, r *http.Request) {
|
|
if h.manager == nil {
|
|
writeError(w, http.StatusInternalServerError, "task manager not initialized")
|
|
return
|
|
}
|
|
|
|
// Extract task ID from path
|
|
taskID := r.PathValue("id")
|
|
if taskID == "" {
|
|
writeError(w, http.StatusBadRequest, "task id is required")
|
|
return
|
|
}
|
|
|
|
task, err := h.manager.Get(r.Context(), taskID)
|
|
if err != nil {
|
|
klog.ErrorS(err, "failed to get task", "id", taskID)
|
|
writeError(w, http.StatusNotFound, fmt.Sprintf("task not found: %v", err))
|
|
return
|
|
}
|
|
|
|
response := convertInternalToAPITask(task)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func (h *Handler) ListTasks(w http.ResponseWriter, r *http.Request) {
|
|
if h.manager == nil {
|
|
writeError(w, http.StatusInternalServerError, "task manager not initialized")
|
|
return
|
|
}
|
|
|
|
tasks, err := h.manager.List(r.Context())
|
|
if err != nil {
|
|
klog.ErrorS(err, "failed to list tasks")
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to list tasks: %v", err))
|
|
return
|
|
}
|
|
|
|
response := make([]api.Task, 0, len(tasks))
|
|
for _, task := range tasks {
|
|
if task != nil {
|
|
response = append(response, *convertInternalToAPITask(task))
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
|
|
response := map[string]string{
|
|
"status": "healthy",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func (h *Handler) DeleteTask(w http.ResponseWriter, r *http.Request) {
|
|
if h.manager == nil {
|
|
writeError(w, http.StatusInternalServerError, "task manager not initialized")
|
|
return
|
|
}
|
|
|
|
// Extract task ID from path
|
|
taskID := r.PathValue("id")
|
|
if taskID == "" {
|
|
writeError(w, http.StatusBadRequest, "task id is required")
|
|
return
|
|
}
|
|
|
|
err := h.manager.Delete(r.Context(), taskID)
|
|
if err != nil {
|
|
klog.ErrorS(err, "failed to delete task", "id", taskID)
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to delete task: %v", err))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
klog.InfoS("task deleted via API", "id", taskID)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, code int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
json.NewEncoder(w).Encode(ErrorResponse{
|
|
Code: http.StatusText(code),
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) convertAPIToInternalTask(apiTask *api.Task) *types.Task {
|
|
if apiTask == nil {
|
|
return nil
|
|
}
|
|
task := &types.Task{
|
|
Name: apiTask.Name,
|
|
Process: apiTask.Process,
|
|
PodTemplateSpec: apiTask.PodTemplateSpec,
|
|
}
|
|
task.Status = types.Status{
|
|
State: types.TaskStatePending,
|
|
}
|
|
|
|
return task
|
|
}
|
|
|
|
func convertInternalToAPITask(task *types.Task) *api.Task {
|
|
if task == nil {
|
|
return nil
|
|
}
|
|
|
|
apiTask := &api.Task{
|
|
Name: task.Name,
|
|
Process: task.Process,
|
|
PodTemplateSpec: task.PodTemplateSpec,
|
|
}
|
|
|
|
if task.Process != nil && len(task.Status.SubStatuses) > 0 {
|
|
sub := task.Status.SubStatuses[0]
|
|
apiStatus := &api.ProcessStatus{}
|
|
|
|
if task.Status.State == types.TaskStateTimeout {
|
|
term := &api.Terminated{
|
|
ExitCode: 137,
|
|
Reason: sub.Reason,
|
|
Message: sub.Message,
|
|
}
|
|
if sub.StartedAt != nil {
|
|
term.StartedAt = metav1.NewTime(*sub.StartedAt)
|
|
}
|
|
term.FinishedAt = metav1.Now()
|
|
apiStatus.Terminated = term
|
|
} else if sub.FinishedAt != nil {
|
|
term := &api.Terminated{
|
|
ExitCode: int32(sub.ExitCode),
|
|
Reason: sub.Reason,
|
|
Message: sub.Message,
|
|
}
|
|
term.FinishedAt = metav1.NewTime(*sub.FinishedAt)
|
|
if sub.StartedAt != nil {
|
|
term.StartedAt = metav1.NewTime(*sub.StartedAt)
|
|
}
|
|
apiStatus.Terminated = term
|
|
} else if sub.StartedAt != nil {
|
|
apiStatus.Running = &api.Running{
|
|
StartedAt: metav1.NewTime(*sub.StartedAt),
|
|
}
|
|
} else {
|
|
apiStatus.Waiting = &api.Waiting{
|
|
Reason: sub.Reason,
|
|
Message: sub.Message,
|
|
}
|
|
}
|
|
apiTask.ProcessStatus = apiStatus
|
|
}
|
|
|
|
if task.PodTemplateSpec != nil {
|
|
podStatus := &corev1.PodStatus{
|
|
Phase: corev1.PodUnknown,
|
|
}
|
|
|
|
switch task.Status.State {
|
|
case types.TaskStatePending:
|
|
podStatus.Phase = corev1.PodPending
|
|
case types.TaskStateRunning:
|
|
podStatus.Phase = corev1.PodRunning
|
|
case types.TaskStateSucceeded:
|
|
podStatus.Phase = corev1.PodSucceeded
|
|
case types.TaskStateFailed:
|
|
podStatus.Phase = corev1.PodFailed
|
|
}
|
|
|
|
for _, sub := range task.Status.SubStatuses {
|
|
cs := corev1.ContainerStatus{
|
|
Name: sub.Name,
|
|
}
|
|
if sub.FinishedAt != nil {
|
|
cs.State.Terminated = &corev1.ContainerStateTerminated{
|
|
ExitCode: int32(sub.ExitCode),
|
|
Reason: sub.Reason,
|
|
Message: sub.Message,
|
|
FinishedAt: metav1.NewTime(*sub.FinishedAt),
|
|
}
|
|
if sub.StartedAt != nil {
|
|
cs.State.Terminated.StartedAt = metav1.NewTime(*sub.StartedAt)
|
|
}
|
|
} else if sub.StartedAt != nil {
|
|
cs.State.Running = &corev1.ContainerStateRunning{
|
|
StartedAt: metav1.NewTime(*sub.StartedAt),
|
|
}
|
|
cs.Ready = true
|
|
} else {
|
|
cs.State.Waiting = &corev1.ContainerStateWaiting{
|
|
Reason: sub.Reason,
|
|
Message: sub.Message,
|
|
}
|
|
}
|
|
podStatus.ContainerStatuses = append(podStatus.ContainerStatuses, cs)
|
|
}
|
|
|
|
allReady := len(podStatus.ContainerStatuses) > 0
|
|
for _, cs := range podStatus.ContainerStatuses {
|
|
if !cs.Ready {
|
|
allReady = false
|
|
break
|
|
}
|
|
}
|
|
readyStatus := corev1.ConditionFalse
|
|
if allReady {
|
|
readyStatus = corev1.ConditionTrue
|
|
}
|
|
|
|
var latestTransition time.Time
|
|
for _, sub := range task.Status.SubStatuses {
|
|
if sub.StartedAt != nil && sub.StartedAt.After(latestTransition) {
|
|
latestTransition = *sub.StartedAt
|
|
}
|
|
if sub.FinishedAt != nil && sub.FinishedAt.After(latestTransition) {
|
|
latestTransition = *sub.FinishedAt
|
|
}
|
|
}
|
|
ltt := metav1.NewTime(latestTransition)
|
|
if latestTransition.IsZero() {
|
|
ltt = metav1.Now()
|
|
}
|
|
|
|
podStatus.Conditions = append(podStatus.Conditions,
|
|
corev1.PodCondition{
|
|
Type: corev1.PodReady,
|
|
Status: readyStatus,
|
|
LastTransitionTime: ltt,
|
|
},
|
|
corev1.PodCondition{
|
|
Type: corev1.ContainersReady,
|
|
Status: readyStatus,
|
|
LastTransitionTime: ltt,
|
|
},
|
|
)
|
|
|
|
apiTask.PodStatus = podStatus
|
|
}
|
|
|
|
return apiTask
|
|
}
|