mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
361 lines
12 KiB
Go
361 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp"
|
|
cpDocker "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/docker"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/entitlements"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/registry"
|
|
cpstripe "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/stripe"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const defaultMobileProofOwnerEmail = "pulse-mobile-ga-proof@pulserelay.pro"
|
|
|
|
type mobileProofRuntime struct {
|
|
cfg *cloudcp.CPConfig
|
|
registry *registry.TenantRegistry
|
|
docker *cpDocker.Manager
|
|
provisioner *cpstripe.Provisioner
|
|
}
|
|
|
|
func newMobileProofCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "mobile-proof",
|
|
Short: "Operate disposable hosted workspaces for Pulse Mobile release proofs",
|
|
Long: "Operate disposable hosted workspaces for Pulse Mobile release proofs.\n\n" +
|
|
"These commands intentionally use the normal hosted workspace provisioner and refuse\n" +
|
|
"customer-shaped accounts or tenants unless explicitly overridden.",
|
|
}
|
|
cmd.AddCommand(newMobileProofCreateAccountCmd())
|
|
cmd.AddCommand(newMobileProofCreateWorkspaceCmd())
|
|
cmd.AddCommand(newMobileProofDeleteWorkspaceCmd())
|
|
return cmd
|
|
}
|
|
|
|
func newMobileProofCreateAccountCmd() *cobra.Command {
|
|
var accountID string
|
|
var displayName string
|
|
var allowNonProofAccount bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "create-account",
|
|
Short: "Create a disposable MSP account for Pulse Mobile proof",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
rt, err := newMobileProofRuntime(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rt.close()
|
|
|
|
accountID = strings.TrimSpace(accountID)
|
|
if accountID == "" {
|
|
generated, err := registry.GenerateAccountID()
|
|
if err != nil {
|
|
return fmt.Errorf("generate proof account id: %w", err)
|
|
}
|
|
accountID = generated
|
|
}
|
|
displayName = strings.TrimSpace(displayName)
|
|
if displayName == "" {
|
|
displayName = fmt.Sprintf("Pulse Mobile GA Proof Account %s", time.Now().UTC().Format("20060102T150405Z"))
|
|
}
|
|
|
|
account := ®istry.Account{
|
|
ID: accountID,
|
|
Kind: registry.AccountKindMSP,
|
|
DisplayName: displayName,
|
|
}
|
|
if !allowNonProofAccount && !looksLikeMobileProofAccount(account) {
|
|
return fmt.Errorf("refusing to create non-proof account %s; display name or account id must contain proof/canary/mobile/rehearsal/seed", accountID)
|
|
}
|
|
if err := rt.registry.CreateAccount(account); err != nil {
|
|
return fmt.Errorf("create mobile proof account: %w", err)
|
|
}
|
|
|
|
fmt.Printf("account_id=%s\n", account.ID)
|
|
fmt.Printf("kind=%s\n", account.Kind)
|
|
fmt.Printf("display_name=%s\n", account.DisplayName)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&accountID, "account-id", "", "Optional account ID; generated when omitted")
|
|
cmd.Flags().StringVar(&displayName, "display-name", "", "Proof account display name; defaults to a Pulse Mobile GA Proof Account timestamp")
|
|
cmd.Flags().BoolVar(&allowNonProofAccount, "allow-non-proof-account", false, "Allow creation of an account that does not look like a proof account")
|
|
return cmd
|
|
}
|
|
|
|
func newMobileProofCreateWorkspaceCmd() *cobra.Command {
|
|
var accountID string
|
|
var displayName string
|
|
var ownerEmail string
|
|
var allowNonProofAccount bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "create-workspace",
|
|
Short: "Create a disposable hosted workspace for Pulse Mobile proof",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
rt, err := newMobileProofRuntime(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rt.close()
|
|
|
|
accountID = strings.TrimSpace(accountID)
|
|
if accountID == "" {
|
|
return fmt.Errorf("--account-id is required")
|
|
}
|
|
if strings.TrimSpace(displayName) == "" {
|
|
displayName = fmt.Sprintf("Pulse Mobile GA Proof %s", time.Now().UTC().Format("20060102T150405Z"))
|
|
}
|
|
ownerEmail = strings.ToLower(strings.TrimSpace(ownerEmail))
|
|
if ownerEmail == "" {
|
|
ownerEmail = defaultMobileProofOwnerEmail
|
|
}
|
|
|
|
account, err := rt.registry.GetAccount(accountID)
|
|
if err != nil {
|
|
return fmt.Errorf("load proof account %s: %w", accountID, err)
|
|
}
|
|
if account == nil {
|
|
return fmt.Errorf("account %s not found", accountID)
|
|
}
|
|
if !allowNonProofAccount && !looksLikeMobileProofAccount(account) {
|
|
return fmt.Errorf("refusing to create mobile proof workspace under non-proof account %s; pass --allow-non-proof-account only after confirming this is not a customer account", accountID)
|
|
}
|
|
if !strings.Contains(strings.ToLower(displayName), "proof") && !strings.Contains(strings.ToLower(displayName), "canary") {
|
|
return fmt.Errorf("--display-name must contain proof or canary")
|
|
}
|
|
|
|
tenant, err := rt.provisioner.ProvisionWorkspaceForOwner(cmd.Context(), accountID, displayName, ownerEmail)
|
|
if err != nil {
|
|
return fmt.Errorf("create mobile proof workspace: %w", err)
|
|
}
|
|
|
|
printMobileProofTenant(rt.cfg, tenant)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&accountID, "account-id", "", "Existing proof account ID that owns the disposable workspace")
|
|
cmd.Flags().StringVar(&displayName, "display-name", "", "Proof workspace display name; defaults to a Pulse Mobile GA Proof timestamp")
|
|
cmd.Flags().StringVar(&ownerEmail, "owner-email", defaultMobileProofOwnerEmail, "Owner email to seed into the hosted workspace metadata")
|
|
cmd.Flags().BoolVar(&allowNonProofAccount, "allow-non-proof-account", false, "Allow creation under an account that does not look like a proof account")
|
|
return cmd
|
|
}
|
|
|
|
func newMobileProofDeleteWorkspaceCmd() *cobra.Command {
|
|
var tenantID string
|
|
var allowNonProofTenant bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "delete-workspace",
|
|
Short: "Soft-delete a disposable hosted workspace created for Pulse Mobile proof",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
rt, err := newMobileProofRuntime(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rt.close()
|
|
|
|
tenantID = strings.TrimSpace(tenantID)
|
|
if tenantID == "" {
|
|
return fmt.Errorf("--tenant-id is required")
|
|
}
|
|
|
|
tenant, err := rt.registry.Get(tenantID)
|
|
if err != nil {
|
|
return fmt.Errorf("load tenant %s: %w", tenantID, err)
|
|
}
|
|
if tenant == nil {
|
|
return fmt.Errorf("tenant %s not found", tenantID)
|
|
}
|
|
account, err := rt.registry.GetAccount(tenant.AccountID)
|
|
if err != nil {
|
|
return fmt.Errorf("load tenant account %s: %w", tenant.AccountID, err)
|
|
}
|
|
if !allowNonProofTenant && !looksLikeMobileProofTenant(tenant, account) {
|
|
return fmt.Errorf("refusing to delete non-proof tenant %s; pass --allow-non-proof-tenant only after confirming this is not a customer workspace", tenantID)
|
|
}
|
|
|
|
previousState := tenant.State
|
|
tenant.State = registry.TenantStateDeleting
|
|
if err := rt.registry.Update(tenant); err != nil {
|
|
return fmt.Errorf("mark tenant deleting: %w", err)
|
|
}
|
|
if err := rt.provisioner.DeprovisionWorkspaceContainer(cmd.Context(), tenant); err != nil {
|
|
return fmt.Errorf("deprovision tenant container: %w", err)
|
|
}
|
|
tenant.ContainerID = ""
|
|
tenant.State = registry.TenantStateDeleted
|
|
if err := rt.registry.Update(tenant); err != nil {
|
|
return fmt.Errorf("mark tenant deleted: %w", err)
|
|
}
|
|
|
|
fmt.Printf("tenant_id=%s\n", tenant.ID)
|
|
fmt.Printf("account_id=%s\n", tenant.AccountID)
|
|
fmt.Printf("previous_state=%s\n", previousState)
|
|
fmt.Printf("state=%s\n", tenant.State)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&tenantID, "tenant-id", "", "Disposable proof tenant ID to delete")
|
|
cmd.Flags().BoolVar(&allowNonProofTenant, "allow-non-proof-tenant", false, "Allow deletion of a tenant that does not look like a proof tenant")
|
|
return cmd
|
|
}
|
|
|
|
func newMobileProofRuntime(ctx context.Context) (*mobileProofRuntime, error) {
|
|
cfg, err := cloudcp.LoadConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load control plane config: %w", err)
|
|
}
|
|
if err := os.MkdirAll(cfg.TenantsDir(), 0o755); err != nil {
|
|
return nil, fmt.Errorf("create tenants dir: %w", err)
|
|
}
|
|
if err := os.MkdirAll(cfg.ControlPlaneDir(), 0o755); err != nil {
|
|
return nil, fmt.Errorf("create control-plane dir: %w", err)
|
|
}
|
|
|
|
reg, err := registry.NewTenantRegistry(cfg.ControlPlaneDir())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open tenant registry: %w", err)
|
|
}
|
|
|
|
dockerMgr, err := cpDocker.NewManager(cpDocker.ManagerConfig{
|
|
Image: cfg.PulseImage,
|
|
Network: cfg.DockerNetwork,
|
|
BaseDomain: mobileProofBaseDomainFromURL(cfg.BaseURL),
|
|
TrialActivationPublicKey: cfg.TrialActivationPublicKey,
|
|
TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
|
|
MemoryLimit: cfg.TenantMemoryLimit,
|
|
CPUShares: cfg.TenantCPUShares,
|
|
TenantLogMaxSize: cfg.TenantLogMaxSize,
|
|
TenantLogMaxFile: cfg.TenantLogMaxFile,
|
|
})
|
|
if err != nil {
|
|
reg.Close()
|
|
return nil, fmt.Errorf("create docker manager: %w", err)
|
|
}
|
|
|
|
hostedEntitlements := entitlements.NewService(reg, cfg.BaseURL, cfg.TrialActivationPrivateKey)
|
|
provisioner := cpstripe.NewProvisioner(
|
|
reg,
|
|
cfg.TenantsDir(),
|
|
dockerMgr,
|
|
nil,
|
|
cfg.BaseURL,
|
|
nil,
|
|
cfg.EmailFrom,
|
|
cfg.AllowDockerlessProvisioning,
|
|
cpstripe.WithHostedEntitlementService(hostedEntitlements),
|
|
cpstripe.WithTrialActivationPrivateKey(cfg.TrialActivationPrivateKey),
|
|
cpstripe.WithAdmissionCheck(func(checkCtx context.Context) error {
|
|
return cloudcp.EnforceStorageAdmission(checkCtx, cfg, dockerMgr)
|
|
}),
|
|
)
|
|
|
|
return &mobileProofRuntime{
|
|
cfg: cfg,
|
|
registry: reg,
|
|
docker: dockerMgr,
|
|
provisioner: provisioner,
|
|
}, nil
|
|
}
|
|
|
|
func (rt *mobileProofRuntime) close() {
|
|
if rt == nil {
|
|
return
|
|
}
|
|
if rt.docker != nil {
|
|
rt.docker.Close()
|
|
}
|
|
if rt.registry != nil {
|
|
rt.registry.Close()
|
|
}
|
|
}
|
|
|
|
func looksLikeMobileProofAccount(account *registry.Account) bool {
|
|
if account == nil {
|
|
return false
|
|
}
|
|
return containsMobileProofMarker(account.ID, account.DisplayName) ||
|
|
strings.HasPrefix(strings.ToLower(strings.TrimSpace(account.ID)), "a_msp_") ||
|
|
strings.Contains(strings.ToLower(strings.TrimSpace(account.ID)), "ownerseed")
|
|
}
|
|
|
|
func looksLikeMobileProofTenant(tenant *registry.Tenant, account *registry.Account) bool {
|
|
if tenant == nil {
|
|
return false
|
|
}
|
|
return containsMobileProofMarker(
|
|
tenant.ID,
|
|
tenant.AccountID,
|
|
tenant.Email,
|
|
tenant.DisplayName,
|
|
tenant.StripeCustomerID,
|
|
tenant.StripeSubscriptionID,
|
|
tenant.StripePriceID,
|
|
tenant.PlanVersion,
|
|
) || looksLikeMobileProofAccount(account)
|
|
}
|
|
|
|
func containsMobileProofMarker(values ...string) bool {
|
|
for _, value := range values {
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
for _, marker := range []string{"mobile", "proof", "canary", "rehearsal", "seed"} {
|
|
if strings.Contains(normalized, marker) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mobileProofBaseDomainFromURL(baseURL string) string {
|
|
domain := strings.TrimSpace(baseURL)
|
|
for _, prefix := range []string{"https://", "http://"} {
|
|
if strings.HasPrefix(domain, prefix) {
|
|
domain = strings.TrimPrefix(domain, prefix)
|
|
break
|
|
}
|
|
}
|
|
if idx := strings.IndexAny(domain, ":/"); idx >= 0 {
|
|
domain = domain[:idx]
|
|
}
|
|
return domain
|
|
}
|
|
|
|
func printMobileProofTenant(cfg *cloudcp.CPConfig, tenant *registry.Tenant) {
|
|
if tenant == nil {
|
|
return
|
|
}
|
|
baseDomain := ""
|
|
if cfg != nil {
|
|
baseDomain = mobileProofBaseDomainFromURL(cfg.BaseURL)
|
|
}
|
|
publicURL := ""
|
|
if baseDomain != "" {
|
|
publicURL = fmt.Sprintf("https://%s.%s", strings.ToLower(tenant.ID), baseDomain)
|
|
}
|
|
|
|
fmt.Printf("tenant_id=%s\n", tenant.ID)
|
|
fmt.Printf("account_id=%s\n", tenant.AccountID)
|
|
fmt.Printf("display_name=%s\n", tenant.DisplayName)
|
|
fmt.Printf("state=%s\n", tenant.State)
|
|
fmt.Printf("plan_version=%s\n", tenant.PlanVersion)
|
|
fmt.Printf("container_id=%s\n", tenant.ContainerID)
|
|
if publicURL != "" {
|
|
fmt.Printf("public_url=%s\n", publicURL)
|
|
fmt.Printf("onboarding_url=%s/api/onboarding/qr\n", publicURL)
|
|
}
|
|
}
|