- store/sqlite: 75.4% (repos + db coverage) - host/sub2api: 80.8% (httptest mock server, pure function tests) - app: 74.2% (handler error paths, NewActionSet closures) - pack: 72.4% - provision: 75.2% - access: 77.3% - config: 94.7% (lookup mock tests) All tests pass: build, vet, race, coverage gates.
639 lines
26 KiB
Go
639 lines
26 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sub2api-cn-relay-manager/internal/host/sub2api"
|
|
"sub2api-cn-relay-manager/internal/pack"
|
|
"sub2api-cn-relay-manager/internal/provision"
|
|
"sub2api-cn-relay-manager/internal/store/sqlite"
|
|
)
|
|
|
|
type ActionSet struct {
|
|
InstallPack func(context.Context, InstallPackRequest) (provision.PackInstallResult, error)
|
|
BatchDetail func(context.Context, BatchDetailRequest) (provision.BatchDetailResult, error)
|
|
GetProviderStatus func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)
|
|
GetProviderResources func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)
|
|
GetProviderAccessStatus func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)
|
|
PreviewProvider func(context.Context, PreviewProviderRequest) (provision.PreviewReport, error)
|
|
ImportProvider func(context.Context, ImportProviderRequest) (provision.RuntimeImportResult, error)
|
|
RollbackProvider func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error)
|
|
ReconcileProvider func(context.Context, ReconcileProviderRequest) (provision.ReconcileResult, error)
|
|
}
|
|
|
|
type InstallPackRequest struct {
|
|
HostBaseURL string `json:"host_base_url"`
|
|
HostAPIKey string `json:"host_api_key"`
|
|
HostBearerToken string `json:"host_bearer_token"`
|
|
PackPath string `json:"pack_path"`
|
|
}
|
|
|
|
type BatchDetailRequest struct {
|
|
BatchID int64
|
|
}
|
|
|
|
type ProviderQueryRequest struct {
|
|
ProviderID string
|
|
PackID string
|
|
}
|
|
|
|
type RollbackProviderRequest struct {
|
|
HostBaseURL string `json:"host_base_url"`
|
|
HostAPIKey string `json:"host_api_key"`
|
|
HostBearerToken string `json:"host_bearer_token"`
|
|
PackPath string `json:"pack_path"`
|
|
ProviderID string `json:"provider_id"`
|
|
}
|
|
|
|
type ReconcileProviderRequest struct {
|
|
HostBaseURL string `json:"host_base_url"`
|
|
HostAPIKey string `json:"host_api_key"`
|
|
HostBearerToken string `json:"host_bearer_token"`
|
|
PackPath string `json:"pack_path"`
|
|
ProviderID string `json:"provider_id"`
|
|
AccessAPIKey string `json:"access_api_key"`
|
|
}
|
|
|
|
type PreviewProviderRequest struct {
|
|
HostBaseURL string `json:"host_base_url"`
|
|
HostAPIKey string `json:"host_api_key"`
|
|
HostBearerToken string `json:"host_bearer_token"`
|
|
PackPath string `json:"pack_path"`
|
|
ProviderID string `json:"provider_id"`
|
|
Keys []string `json:"keys"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
type ImportProviderRequest struct {
|
|
HostBaseURL string `json:"host_base_url"`
|
|
HostAPIKey string `json:"host_api_key"`
|
|
HostBearerToken string `json:"host_bearer_token"`
|
|
PackPath string `json:"pack_path"`
|
|
ProviderID string `json:"provider_id"`
|
|
Keys []string `json:"keys"`
|
|
Mode string `json:"mode"`
|
|
AccessMode string `json:"access_mode"`
|
|
AccessAPIKey string `json:"access_api_key"`
|
|
SubscriptionUsers []string `json:"subscription_users"`
|
|
SubscriptionDays int `json:"subscription_days"`
|
|
}
|
|
|
|
type httpError struct {
|
|
StatusCode int `json:"-"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
UpstreamStatus int `json:"upstream_status,omitempty"`
|
|
}
|
|
|
|
func (e *httpError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
func NewAPIHandler(adminToken string, actions ActionSet) http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /healthz", healthz)
|
|
mux.Handle("GET /api/import-batches/{batchID}", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleBatchDetail(w, r, actions.BatchDetail)
|
|
})))
|
|
mux.Handle("GET /api/providers/{providerID}/status", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleProviderStatus(w, r, actions.GetProviderStatus)
|
|
})))
|
|
mux.Handle("GET /api/providers/{providerID}/resources", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleProviderResources(w, r, actions.GetProviderResources)
|
|
})))
|
|
mux.Handle("GET /api/providers/{providerID}/access/status", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleProviderAccessStatus(w, r, actions.GetProviderAccessStatus)
|
|
})))
|
|
mux.Handle("POST /api/packs/install", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleInstallPack(w, r, actions.InstallPack)
|
|
})))
|
|
mux.Handle("POST /api/providers/{providerID}/preview-import", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handlePreviewProvider(w, r, actions.PreviewProvider)
|
|
})))
|
|
mux.Handle("POST /api/providers/{providerID}/import", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleImportProvider(w, r, actions.ImportProvider)
|
|
})))
|
|
mux.Handle("POST /api/providers/{providerID}/rollback", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleRollbackProvider(w, r, actions.RollbackProvider)
|
|
})))
|
|
mux.Handle("POST /api/providers/{providerID}/reconcile", requireAdminToken(adminToken, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handleReconcileProvider(w, r, actions.ReconcileProvider)
|
|
})))
|
|
return mux
|
|
}
|
|
|
|
func healthz(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("ok"))
|
|
}
|
|
|
|
func requireAdminToken(token string, next http.Handler) http.Handler {
|
|
if strings.TrimSpace(token) == "" {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "admin token is not configured"})
|
|
})
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if bearerToken(r) != token {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "missing or invalid admin token"})
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func bearerToken(r *http.Request) string {
|
|
header := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
if !strings.HasPrefix(strings.ToLower(header), "bearer ") {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(header[len("Bearer "):])
|
|
}
|
|
|
|
func handleInstallPack(w http.ResponseWriter, r *http.Request, fn func(context.Context, InstallPackRequest) (provision.PackInstallResult, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "install-pack action is not configured"})
|
|
return
|
|
}
|
|
var req InstallPackRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
result, err := fn(r.Context(), req)
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
providers := make([]map[string]string, 0, len(result.Providers))
|
|
for _, provider := range result.Providers {
|
|
providers = append(providers, map[string]string{
|
|
"provider_id": provider.ProviderID,
|
|
"display_name": provider.DisplayName,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"pack_id": result.Pack.PackID,
|
|
"version": result.Pack.Version,
|
|
"host_version": result.HostVersion,
|
|
"already_installed": result.AlreadyInstalled,
|
|
"providers": providers,
|
|
})
|
|
}
|
|
|
|
func handleBatchDetail(w http.ResponseWriter, r *http.Request, fn func(context.Context, BatchDetailRequest) (provision.BatchDetailResult, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "batch-detail action is not configured"})
|
|
return
|
|
}
|
|
batchID, err := strconv.ParseInt(r.PathValue("batchID"), 10, 64)
|
|
if err != nil || batchID <= 0 {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "batch_id must be a positive integer"})
|
|
return
|
|
}
|
|
result, err := fn(r.Context(), BatchDetailRequest{BatchID: batchID})
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(result.Items))
|
|
for _, item := range result.Items {
|
|
items = append(items, map[string]any{
|
|
"id": item.ID,
|
|
"batch_id": item.BatchID,
|
|
"key_fingerprint": item.KeyFingerprint,
|
|
"account_status": item.AccountStatus,
|
|
"probe_summary_json": item.ProbeSummaryJSON,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"batch": map[string]any{
|
|
"id": result.Batch.ID,
|
|
"host_id": result.Batch.HostID,
|
|
"pack_id": result.Batch.PackID,
|
|
"provider_id": result.Batch.ProviderID,
|
|
"mode": result.Batch.Mode,
|
|
"batch_status": result.Batch.BatchStatus,
|
|
"access_status": result.Batch.AccessStatus,
|
|
},
|
|
"items": items,
|
|
"managed_resources": result.ManagedResources,
|
|
"access_closures": result.AccessClosures,
|
|
"reconcile_runs": result.ReconcileRuns,
|
|
"items_count": len(result.Items),
|
|
"managed_count": len(result.ManagedResources),
|
|
"access_count": len(result.AccessClosures),
|
|
"reconcile_count": len(result.ReconcileRuns),
|
|
})
|
|
}
|
|
|
|
func handleProviderStatus(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "provider-status action is not configured"})
|
|
return
|
|
}
|
|
result, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: strings.TrimSpace(r.URL.Query().Get("pack_id"))})
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"host": map[string]any{"host_id": result.Host.HostID, "base_url": result.Host.BaseURL, "host_version": result.Host.HostVersion},
|
|
"pack": map[string]any{"pack_id": result.Pack.PackID, "version": result.Pack.Version},
|
|
"provider": map[string]any{"provider_id": result.Provider.ProviderID, "display_name": result.Provider.DisplayName, "platform": result.Provider.Platform},
|
|
"batch": map[string]any{"id": result.Batch.ID, "batch_status": result.Batch.BatchStatus, "access_status": result.Batch.AccessStatus, "mode": result.Batch.Mode},
|
|
"provider_status": result.ProviderStatus,
|
|
"latest_access_status": result.LatestAccessStatus,
|
|
"latest_reconcile_status": result.LatestReconcileStatus,
|
|
"latest_reconcile_summary": result.LatestReconcileSummary,
|
|
"managed_resources_count": len(result.ManagedResources),
|
|
"access_closures_count": len(result.AccessClosures),
|
|
"reconcile_runs_count": len(result.ReconcileRuns),
|
|
})
|
|
}
|
|
|
|
func handleProviderAccessStatus(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "provider-access-status action is not configured"})
|
|
return
|
|
}
|
|
result, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: strings.TrimSpace(r.URL.Query().Get("pack_id"))})
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
latestClosure := map[string]any{}
|
|
if n := len(result.AccessClosures); n > 0 {
|
|
closure := result.AccessClosures[n-1]
|
|
latestClosure = map[string]any{"id": closure.ID, "closure_type": closure.ClosureType, "status": closure.Status, "details_json": closure.DetailsJSON}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"provider_id": result.Provider.ProviderID,
|
|
"pack_id": result.Pack.PackID,
|
|
"batch_id": result.Batch.ID,
|
|
"batch_access_status": result.Batch.AccessStatus,
|
|
"latest_access_status": result.LatestAccessStatus,
|
|
"closures_count": len(result.AccessClosures),
|
|
"latest_closure": latestClosure,
|
|
})
|
|
}
|
|
|
|
func handleProviderResources(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "provider-resources action is not configured"})
|
|
return
|
|
}
|
|
result, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: strings.TrimSpace(r.URL.Query().Get("pack_id"))})
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
resources := make([]map[string]any, 0, len(result.ManagedResources))
|
|
for _, resource := range result.ManagedResources {
|
|
resources = append(resources, map[string]any{"id": resource.ID, "resource_type": resource.ResourceType, "host_resource_id": resource.HostResourceID, "resource_name": resource.ResourceName})
|
|
}
|
|
accessClosures := make([]map[string]any, 0, len(result.AccessClosures))
|
|
for _, closure := range result.AccessClosures {
|
|
accessClosures = append(accessClosures, map[string]any{"id": closure.ID, "closure_type": closure.ClosureType, "status": closure.Status, "details_json": closure.DetailsJSON})
|
|
}
|
|
reconcileRuns := make([]map[string]any, 0, len(result.ReconcileRuns))
|
|
for _, run := range result.ReconcileRuns {
|
|
reconcileRuns = append(reconcileRuns, map[string]any{"id": run.ID, "status": run.Status, "summary_json": run.SummaryJSON})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"provider_id": result.Provider.ProviderID,
|
|
"pack_id": result.Pack.PackID,
|
|
"batch_id": result.Batch.ID,
|
|
"resources": resources,
|
|
"access_closures": accessClosures,
|
|
"reconcile_runs": reconcileRuns,
|
|
})
|
|
}
|
|
|
|
func handlePreviewProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, PreviewProviderRequest) (provision.PreviewReport, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "preview-provider action is not configured"})
|
|
return
|
|
}
|
|
var req PreviewProviderRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
req.ProviderID = r.PathValue("providerID")
|
|
result, err := fn(r.Context(), req)
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"accepted_keys_count": len(result.AcceptedKeys),
|
|
"names": result.Names,
|
|
"decisions": result.Decisions,
|
|
})
|
|
}
|
|
|
|
func handleImportProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, ImportProviderRequest) (provision.RuntimeImportResult, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "import-provider action is not configured"})
|
|
return
|
|
}
|
|
var req ImportProviderRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
req.ProviderID = r.PathValue("providerID")
|
|
result, err := fn(r.Context(), req)
|
|
if err != nil {
|
|
payload := map[string]any{
|
|
"batch_id": result.BatchID,
|
|
"batch_status": result.Report.BatchStatus,
|
|
"provider_status": result.Report.ProviderStatus,
|
|
"access_status": result.Report.AccessStatus,
|
|
"accepted_keys_count": len(result.Report.AcceptedKeys),
|
|
"accounts_count": len(result.Report.Accounts),
|
|
"gateway": result.Report.Gateway,
|
|
"error": classifyError(err),
|
|
}
|
|
statusCode := http.StatusConflict
|
|
if result.BatchID == 0 {
|
|
statusCode = classifyError(err).StatusCode
|
|
}
|
|
writeJSON(w, statusCode, payload)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"batch_id": result.BatchID,
|
|
"batch_status": result.Report.BatchStatus,
|
|
"provider_status": result.Report.ProviderStatus,
|
|
"access_status": result.Report.AccessStatus,
|
|
"accepted_keys_count": len(result.Report.AcceptedKeys),
|
|
"accounts_count": len(result.Report.Accounts),
|
|
"group": result.Report.Group,
|
|
"channel": result.Report.Channel,
|
|
"plan": result.Report.Plan,
|
|
"gateway": result.Report.Gateway,
|
|
})
|
|
}
|
|
|
|
func handleRollbackProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "rollback-provider action is not configured"})
|
|
return
|
|
}
|
|
var req RollbackProviderRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
req.ProviderID = r.PathValue("providerID")
|
|
result, err := fn(r.Context(), req)
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"provider_id": req.ProviderID,
|
|
"deleted_accounts": result.AccountsDeleted,
|
|
"deleted_plans": result.PlansDeleted,
|
|
"deleted_channels": result.ChannelsDeleted,
|
|
"deleted_groups": result.GroupsDeleted,
|
|
})
|
|
}
|
|
|
|
func handleReconcileProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, ReconcileProviderRequest) (provision.ReconcileResult, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "reconcile-provider action is not configured"})
|
|
return
|
|
}
|
|
var req ReconcileProviderRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
req.ProviderID = r.PathValue("providerID")
|
|
result, err := fn(r.Context(), req)
|
|
if err != nil {
|
|
writeHTTPError(w, classifyError(err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"provider_id": req.ProviderID,
|
|
"batch_id": result.BatchID,
|
|
"status": result.Status,
|
|
"missing_count": result.MissingCount,
|
|
"extra_count": result.ExtraCount,
|
|
"summary": result.Summary,
|
|
})
|
|
}
|
|
|
|
func decodeJSON(r *http.Request, dest any) *httpError {
|
|
decoder := json.NewDecoder(r.Body)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(dest); err != nil {
|
|
return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: fmt.Sprintf("decode request body: %v", err)}
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != nil && !errors.Is(err, io.EOF) {
|
|
return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "request body must contain a single JSON object"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeHTTPError(w http.ResponseWriter, err *httpError) {
|
|
if err == nil {
|
|
err = &httpError{StatusCode: http.StatusInternalServerError, Code: "internal_error", Message: "internal server error"}
|
|
}
|
|
writeJSON(w, err.StatusCode, map[string]any{"error": err})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, statusCode int, body any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|
|
|
|
func classifyError(err error) *httpError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var requestErr *httpError
|
|
if errors.As(err, &requestErr) {
|
|
return requestErr
|
|
}
|
|
var upstreamErr *sub2api.HTTPError
|
|
if errors.As(err, &upstreamErr) {
|
|
return &httpError{StatusCode: http.StatusBadGateway, Code: "host_request_failed", Message: err.Error(), UpstreamStatus: upstreamErr.StatusCode}
|
|
}
|
|
message := err.Error()
|
|
switch {
|
|
case strings.Contains(message, "already installed") || strings.Contains(message, "checksum drift"):
|
|
return &httpError{StatusCode: http.StatusConflict, Code: "pack_conflict", Message: message}
|
|
case strings.Contains(message, "not found in pack"):
|
|
return &httpError{StatusCode: http.StatusBadRequest, Code: "provider_not_found", Message: message}
|
|
case strings.Contains(message, "pack path") || strings.Contains(message, "pack dir") || strings.Contains(message, "required") || strings.Contains(message, "decode"):
|
|
return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: message}
|
|
default:
|
|
return &httpError{StatusCode: http.StatusInternalServerError, Code: "internal_error", Message: message}
|
|
}
|
|
}
|
|
|
|
func NewActionSet(sqliteDSN string) ActionSet {
|
|
return ActionSet{
|
|
InstallPack: func(ctx context.Context, req InstallPackRequest) (provision.PackInstallResult, error) {
|
|
loadedPack, err := pack.LoadPath(req.PackPath)
|
|
if err != nil {
|
|
return provision.PackInstallResult{}, err
|
|
}
|
|
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
|
if err != nil {
|
|
return provision.PackInstallResult{}, err
|
|
}
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.PackInstallResult{}, err
|
|
}
|
|
defer store.Close()
|
|
service := provision.NewPackInstallService(store, client)
|
|
return service.Install(ctx, provision.PackInstallRequest{Pack: loadedPack})
|
|
},
|
|
BatchDetail: func(ctx context.Context, req BatchDetailRequest) (provision.BatchDetailResult, error) {
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.BatchDetailResult{}, err
|
|
}
|
|
defer store.Close()
|
|
return provision.NewBatchDetailService(store).Get(ctx, req.BatchID)
|
|
},
|
|
GetProviderStatus: func(ctx context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.ProviderSnapshot{}, err
|
|
}
|
|
defer store.Close()
|
|
return provision.NewProviderStatusService(store).GetStatus(ctx, provision.ProviderQuery{ProviderID: req.ProviderID, PackID: req.PackID})
|
|
},
|
|
GetProviderResources: func(ctx context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.ProviderSnapshot{}, err
|
|
}
|
|
defer store.Close()
|
|
return provision.NewProviderStatusService(store).GetResources(ctx, provision.ProviderQuery{ProviderID: req.ProviderID, PackID: req.PackID})
|
|
},
|
|
GetProviderAccessStatus: func(ctx context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.ProviderSnapshot{}, err
|
|
}
|
|
defer store.Close()
|
|
return provision.NewProviderStatusService(store).GetStatus(ctx, provision.ProviderQuery{ProviderID: req.ProviderID, PackID: req.PackID})
|
|
},
|
|
PreviewProvider: func(ctx context.Context, req PreviewProviderRequest) (provision.PreviewReport, error) {
|
|
loadedPack, err := pack.LoadPath(req.PackPath)
|
|
if err != nil {
|
|
return provision.PreviewReport{}, err
|
|
}
|
|
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
|
if err != nil {
|
|
return provision.PreviewReport{}, err
|
|
}
|
|
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
|
if err != nil {
|
|
return provision.PreviewReport{}, err
|
|
}
|
|
service := provision.NewPreviewService(client)
|
|
return service.PreviewImport(ctx, provision.PreviewRequest{Provider: providerManifest, Mode: req.Mode, Keys: req.Keys})
|
|
},
|
|
ImportProvider: func(ctx context.Context, req ImportProviderRequest) (provision.RuntimeImportResult, error) {
|
|
loadedPack, err := pack.LoadPath(req.PackPath)
|
|
if err != nil {
|
|
return provision.RuntimeImportResult{}, err
|
|
}
|
|
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
|
if err != nil {
|
|
return provision.RuntimeImportResult{}, err
|
|
}
|
|
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
|
if err != nil {
|
|
return provision.RuntimeImportResult{}, err
|
|
}
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.RuntimeImportResult{}, err
|
|
}
|
|
defer store.Close()
|
|
subscriptions := make([]provision.SubscriptionTarget, 0, len(req.SubscriptionUsers))
|
|
for _, userID := range req.SubscriptionUsers {
|
|
subscriptions = append(subscriptions, provision.SubscriptionTarget{UserID: userID, DurationDays: req.SubscriptionDays})
|
|
}
|
|
service := provision.NewRuntimeImportService(store, client)
|
|
return service.Import(ctx, provision.RuntimeImportRequest{
|
|
HostBaseURL: req.HostBaseURL,
|
|
Pack: loadedPack,
|
|
Provider: providerManifest,
|
|
Mode: req.Mode,
|
|
Keys: req.Keys,
|
|
Access: provision.AccessRequest{
|
|
Mode: req.AccessMode,
|
|
ProbeAPIKey: req.AccessAPIKey,
|
|
Subscriptions: subscriptions,
|
|
},
|
|
})
|
|
},
|
|
RollbackProvider: func(ctx context.Context, req RollbackProviderRequest) (provision.RollbackReport, error) {
|
|
loadedPack, err := pack.LoadPath(req.PackPath)
|
|
if err != nil {
|
|
return provision.RollbackReport{}, err
|
|
}
|
|
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
|
if err != nil {
|
|
return provision.RollbackReport{}, err
|
|
}
|
|
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
|
if err != nil {
|
|
return provision.RollbackReport{}, err
|
|
}
|
|
service := provision.NewRollbackService(client)
|
|
return service.Rollback(ctx, provision.RollbackRequest{Provider: providerManifest})
|
|
},
|
|
ReconcileProvider: func(ctx context.Context, req ReconcileProviderRequest) (provision.ReconcileResult, error) {
|
|
loadedPack, err := pack.LoadPath(req.PackPath)
|
|
if err != nil {
|
|
return provision.ReconcileResult{}, err
|
|
}
|
|
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
|
if err != nil {
|
|
return provision.ReconcileResult{}, err
|
|
}
|
|
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
|
if err != nil {
|
|
return provision.ReconcileResult{}, err
|
|
}
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return provision.ReconcileResult{}, err
|
|
}
|
|
defer store.Close()
|
|
service := provision.NewReconcileService(store, client)
|
|
return service.Reconcile(ctx, provision.ReconcileRequest{HostBaseURL: req.HostBaseURL, AccessProbeAPIKey: req.AccessAPIKey, Pack: loadedPack, Provider: providerManifest})
|
|
},
|
|
}
|
|
}
|
|
|
|
func findProvider(loaded pack.LoadedPack, providerID string) (pack.ProviderManifest, error) {
|
|
for _, provider := range loaded.Providers {
|
|
if provider.ProviderID == strings.TrimSpace(providerID) {
|
|
return provider, nil
|
|
}
|
|
}
|
|
return pack.ProviderManifest{}, fmt.Errorf("provider %q not found in pack %q", providerID, loaded.Manifest.PackID)
|
|
}
|