Files
sub2api-cn-relay-manager/internal/app/http_api.go
phamnazage-jpg 71cbaf5fa6 test(project): achieve ≥70% package coverage across all internal packages
- 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.
2026-05-15 19:26:25 +08:00

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)
}