mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
- Added environment creation and editing pages with forms for name and description. - Integrated environment listing with options to edit and delete environments. - Updated agent detail page to manage environments followed by agents. - Enhanced API to support environment operations including listing, creating, updating, and deleting environments. - Refactored related components and state management for improved clarity and functionality.
262 lines
7.9 KiB
Go
262 lines
7.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lyzrai/flow/pkg/storage"
|
|
)
|
|
|
|
// Environments API — global, named deploy stages. An environment is a
|
|
// sequencing container: it owns an ordered list of pipelines (the
|
|
// promotion sequence) plus a description. Per-deploy config lives on the
|
|
// nodes, not here. Agents subscribe to environments (see agents.go);
|
|
// dispatch fans out across the followed envs' pipelines.
|
|
//
|
|
// Routes:
|
|
// GET /api/environments
|
|
// POST /api/environments
|
|
// GET /api/environments/{name}
|
|
// PUT /api/environments/{name} (name + description)
|
|
// DELETE /api/environments/{name}
|
|
// POST /api/environments/{name}/pipelines/{pipelineId} (bring a pipeline in — appends)
|
|
// DELETE /api/environments/{name}/pipelines/{pipelineId} (remove a pipeline)
|
|
// PUT /api/environments/{name}/pipelines (reorder; body {"pipelineIds":[...]})
|
|
|
|
// environmentBody is the create/update wire shape. PipelineIDs are
|
|
// managed via the /pipelines sub-routes.
|
|
type environmentBody struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
func (b *environmentBody) normalize() (storage.Environment, error) {
|
|
name := strings.TrimSpace(b.Name)
|
|
if name == "" {
|
|
return storage.Environment{}, errors.New("name is required")
|
|
}
|
|
now := time.Now().UTC()
|
|
return storage.Environment{
|
|
Name: name,
|
|
Description: strings.TrimSpace(b.Description),
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) handleListEnvironments(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
list, err := s.environments.List(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if list == nil {
|
|
list = []*storage.Environment{}
|
|
}
|
|
writeJSON(w, http.StatusOK, list)
|
|
}
|
|
|
|
func (s *Server) handleGetEnvironment(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
e, err := s.environments.GetByName(r.Context(), r.PathValue("name"))
|
|
if err != nil {
|
|
writeStorageErr(w, err, "environment not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, e)
|
|
}
|
|
|
|
func (s *Server) handleCreateEnvironment(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
var body environmentBody
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
env, err := body.normalize()
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
env.ID = newID()
|
|
if err := s.environments.Create(r.Context(), &env); err != nil {
|
|
if errors.Is(err, storage.ErrAlreadyExists) {
|
|
writeError(w, http.StatusConflict, fmt.Errorf("environment %q already exists", env.Name))
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, &env)
|
|
}
|
|
|
|
func (s *Server) handleUpdateEnvironment(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
name := r.PathValue("name")
|
|
existing, err := s.environments.GetByName(r.Context(), name)
|
|
if err != nil {
|
|
writeStorageErr(w, err, "environment not found")
|
|
return
|
|
}
|
|
var body environmentBody
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if body.Name == "" {
|
|
body.Name = name
|
|
}
|
|
updated, err := body.normalize()
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
// Name is immutable through PUT; carry forward identity + pipeline set.
|
|
updated.Name = existing.Name
|
|
updated.ID = existing.ID
|
|
updated.PipelineIDs = existing.PipelineIDs
|
|
updated.CreatedAt = existing.CreatedAt
|
|
updated.UpdatedAt = time.Now().UTC()
|
|
if err := s.environments.Update(r.Context(), &updated); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, &updated)
|
|
}
|
|
|
|
func (s *Server) handleDeleteEnvironment(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
if err := s.environments.Delete(r.Context(), r.PathValue("name")); err != nil {
|
|
writeStorageErr(w, err, "environment not found")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleEnvAddPipeline(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
name := r.PathValue("name")
|
|
pipelineID := r.PathValue("pipelineId")
|
|
env, err := s.environments.GetByName(r.Context(), name)
|
|
if err != nil {
|
|
writeStorageErr(w, err, "environment not found")
|
|
return
|
|
}
|
|
if _, err := s.pipelines.Get(r.Context(), pipelineID); err != nil {
|
|
writeStorageErr(w, err, "pipeline not found")
|
|
return
|
|
}
|
|
if env.HasPipeline(pipelineID) {
|
|
writeJSON(w, http.StatusOK, env) // already in this env
|
|
return
|
|
}
|
|
env.PipelineIDs = append(env.PipelineIDs, pipelineID)
|
|
env.UpdatedAt = time.Now().UTC()
|
|
if err := s.environments.Update(r.Context(), env); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, env)
|
|
}
|
|
|
|
func (s *Server) handleEnvRemovePipeline(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
name := r.PathValue("name")
|
|
pipelineID := r.PathValue("pipelineId")
|
|
env, err := s.environments.GetByName(r.Context(), name)
|
|
if err != nil {
|
|
writeStorageErr(w, err, "environment not found")
|
|
return
|
|
}
|
|
out := env.PipelineIDs[:0]
|
|
for _, p := range env.PipelineIDs {
|
|
if p != pipelineID {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
env.PipelineIDs = out
|
|
env.UpdatedAt = time.Now().UTC()
|
|
if err := s.environments.Update(r.Context(), env); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// handleEnvReorderPipelines replaces the env's pipeline order. The body
|
|
// must be a permutation of the env's current pipeline set — extra or
|
|
// missing IDs are rejected so a stale client can't silently drop a
|
|
// pipeline.
|
|
func (s *Server) handleEnvReorderPipelines(w http.ResponseWriter, r *http.Request) {
|
|
if s.environments == nil {
|
|
writeError(w, http.StatusServiceUnavailable, errors.New("environments store not configured"))
|
|
return
|
|
}
|
|
name := r.PathValue("name")
|
|
env, err := s.environments.GetByName(r.Context(), name)
|
|
if err != nil {
|
|
writeStorageErr(w, err, "environment not found")
|
|
return
|
|
}
|
|
var body struct {
|
|
PipelineIDs []string `json:"pipelineIds"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
cur := map[string]bool{}
|
|
for _, p := range env.PipelineIDs {
|
|
cur[p] = true
|
|
}
|
|
if len(body.PipelineIDs) != len(env.PipelineIDs) {
|
|
writeError(w, http.StatusBadRequest, fmt.Errorf("expected a permutation of %d pipeline ids, got %d", len(env.PipelineIDs), len(body.PipelineIDs)))
|
|
return
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, p := range body.PipelineIDs {
|
|
if !cur[p] {
|
|
writeError(w, http.StatusBadRequest, fmt.Errorf("pipeline %q is not in this environment", p))
|
|
return
|
|
}
|
|
if seen[p] {
|
|
writeError(w, http.StatusBadRequest, fmt.Errorf("pipeline %q listed twice", p))
|
|
return
|
|
}
|
|
seen[p] = true
|
|
}
|
|
env.PipelineIDs = body.PipelineIDs
|
|
env.UpdatedAt = time.Now().UTC()
|
|
if err := s.environments.Update(r.Context(), env); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, env)
|
|
}
|