@@ -278,6 +278,7 @@ public class ApiConstants { | |||
public static String getReferer(ContainerRequest ctx) { return ctx.getHeaderString(REFERER); } | |||
public static final String DETECT_LOCALE = "detect"; | |||
public static final String DETECT_TIMEZONE = "detect"; | |||
public static List<String> getLocales(ContainerRequest ctx, String defaultLocale) { | |||
final List<String> locales = new ArrayList<>(); | |||
@@ -21,6 +21,7 @@ import java.util.List; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
import static org.hibernate.criterion.Restrictions.*; | |||
@Repository @Slf4j | |||
public class AccountPaymentMethodDAO extends AccountOwnedEntityDAO<AccountPaymentMethod> { | |||
@@ -42,6 +43,14 @@ public class AccountPaymentMethodDAO extends AccountOwnedEntityDAO<AccountPaymen | |||
return findByFields("account", account, "paymentMethodType", PaymentMethodType.promotional_credit, "deleted", null); | |||
} | |||
public List<AccountPaymentMethod> findByAccountAndNotPromoAndNotDeleted(String account) { | |||
return list(criteria().add(and( | |||
eq("account", account), | |||
ne("paymentMethodType", PaymentMethodType.promotional_credit), | |||
isNull("deleted") | |||
))); | |||
} | |||
public List<AccountPaymentMethod> findByAccountAndCloud(String accountUuid, String cloud) { | |||
return findByFields("account", accountUuid, "cloud", cloud); | |||
} | |||
@@ -163,16 +163,16 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
@Transient @Getter @Setter private transient Boolean syncPassword = null; | |||
public boolean syncPassword () { return bool(syncPassword); } | |||
public boolean syncPassword () { return syncPassword == null || syncPassword; } | |||
@Transient @Getter @Setter private Boolean launchLock; | |||
public boolean launchLock() { return bool(launchLock); } | |||
@Transient @Getter @Setter private transient Boolean sendErrors = null; | |||
public boolean sendErrors () { return bool(sendErrors); } | |||
public boolean sendErrors () { return sendErrors == null || sendErrors; } | |||
@Transient @Getter @Setter private transient Boolean sendMetrics = null; | |||
public boolean sendMetrics () { return bool(sendMetrics); } | |||
public boolean sendMetrics () { return sendMetrics == null || sendMetrics; } | |||
public BubbleNetwork bubbleNetwork(Account account, | |||
BubbleDomain domain, | |||
@@ -151,13 +151,14 @@ public class AccountOwnedResource<E extends HasAccount, DAO extends AccountOwned | |||
if (!canCreate(req, ctx, caller, request)) return invalid("err.cannotCreate", "Create entity not allowed", request.getName()); | |||
final E toCreate = setReferences(ctx, caller, instantiate(getEntityClass(), request).setAccount(getAccountUuid(ctx))); | |||
final E toCreate = setReferences(ctx, req, caller, instantiate(getEntityClass(), request).setAccount(getAccountUuid(ctx))); | |||
return ok(daoCreate(toCreate)); | |||
} | |||
protected Object daoCreate(E toCreate) { return getDao().create(toCreate); } | |||
protected E setReferences(ContainerRequest ctx, Account caller, E e) { return e; } | |||
protected E setReferences(ContainerRequest ctx, Request req, Account caller, E e) { return setReferences(ctx, caller, e); } | |||
@POST @Path("/{id}") | |||
public Response update(@Context ContainerRequest ctx, | |||
@@ -6,9 +6,11 @@ package bubble.resources.bill; | |||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||
import bubble.dao.bill.AccountPlanDAO; | |||
import bubble.dao.bill.BubblePlanDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.resources.account.AccountOwnedResource; | |||
import bubble.server.BubbleConfiguration; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -33,6 +35,7 @@ public class AccountPaymentMethodsResource extends AccountOwnedResource<AccountP | |||
public static final String PARAM_ALL = "all"; | |||
@Autowired private BubblePlanDAO planDAO; | |||
@Autowired private AccountPlanDAO accountPlanDAO; | |||
@Autowired private BubbleConfiguration configuration; | |||
@@ -78,7 +81,9 @@ public class AccountPaymentMethodsResource extends AccountOwnedResource<AccountP | |||
@Override protected Object daoCreate(AccountPaymentMethod apm) { | |||
if (apm.hasPreferredPlan()) { | |||
final Account account = accountDAO.findByUuid(apm.getAccount()); | |||
accountDAO.update(account.setPreferredPlan(apm.getPreferredPlan())); | |||
final BubblePlan plan = planDAO.findById(apm.getPreferredPlan()); | |||
if (plan == null) throw invalidEx("err.plan.notFound", "plan not found: "+apm.getPreferredPlan()); | |||
accountDAO.update(account.setPreferredPlan(plan.getUuid())); | |||
} | |||
return super.daoCreate(apm); | |||
} | |||
@@ -101,11 +101,18 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
return super.canDelete(ctx, caller, found); | |||
} | |||
@Override protected AccountPlan setReferences(ContainerRequest ctx, Account caller, AccountPlan request) { | |||
@Override protected AccountPlan setReferences(ContainerRequest ctx, Request req, Account caller, AccountPlan request) { | |||
// ensure we have latest account settings (preferredPlan/etc) | |||
caller = accountDAO.findByUuid(caller.getUuid()); | |||
final ValidationResult errors = new ValidationResult(); | |||
if (!request.hasTimezone()) errors.addViolation("err.timezone.required"); | |||
if (!request.hasLocale()) errors.addViolation("err.locale.required"); | |||
if (!request.hasTimezone() || request.getTimezone().equals(DETECT_TIMEZONE)) { | |||
request.setTimezone(geoService.getTimeZone(caller, getRemoteHost(req)).getStandardName()); | |||
} | |||
if (!request.hasLocale() || request.getLocale().equals(DETECT_LOCALE)) { | |||
request.setLocale(geoService.getFirstLocale(account, getRemoteHost(req), normalizeLangHeader(req))); | |||
} | |||
request.setAccount(caller.getUuid()); | |||
if (request.hasSshKey()) { | |||
@@ -115,17 +122,25 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
} else { | |||
request.setSshKey(sshKey.getUuid()); | |||
} | |||
} else if (configuration.isSageLauncher()) { | |||
final List<AccountSshKey> sshKeys = sshKeyDAO.findByAccount(caller.getUuid()); | |||
if (empty(sshKeys)) { | |||
request.setSshKey(null); | |||
} else { | |||
request.setSshKey(sshKeys.get(0).getUuid()); | |||
} | |||
} else { | |||
request.setSshKey(null); // if it's an empty string, make it null (see simple_network test) | |||
} | |||
final BubbleDomain domain = domainDAO.findByAccountAndId(caller.getUuid(), request.getDomain()); | |||
BubbleDomain domain = domainDAO.findByAccountAndId(caller.getUuid(), request.getDomain()); | |||
if (domain == null) { | |||
log.info("setReferences: domain not found: "+request.getDomain()+" for caller: "+caller.getUuid()); | |||
errors.addViolation("err.domain.required"); | |||
final List<BubbleDomain> domains = domainDAO.findByAccount(caller.getUuid()); | |||
if (empty(domains)) return die("setReferences: no domains found for account: "+caller.getUuid()); | |||
domain = domains.get(0); | |||
request.setDomain(domain.getUuid()); | |||
} else { | |||
request.setDomain(domain.getUuid()); | |||
final BubbleNetwork existingNetwork = networkDAO.findByNameAndDomainName(request.getName(), domain.getName()); | |||
if (existingNetwork != null) errors.addViolation("err.name.networkNameAlreadyExists"); | |||
} | |||
@@ -138,7 +153,15 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
if (!validateRegexMatches(HOST_PATTERN, forkHost)) { | |||
errors.addViolation("err.forkHost.invalid"); | |||
} else if (domain != null && !forkHost.endsWith("."+domain.getName())) { | |||
errors.addViolation("err.forkHost.domainMismatch"); | |||
final BubbleDomain foundDomain = domainDAO.findByAccount(caller.getUuid()).stream() | |||
.filter(d -> forkHost.equals("." + d.getName())) | |||
.findFirst().orElse(null); | |||
if (foundDomain == null) { | |||
errors.addViolation("err.forkHost.domain.notFound"); | |||
} else { | |||
request.setDomain(foundDomain.getUuid()); | |||
} | |||
} else if (domain != null) { | |||
request.setName(domain.networkFromFqdn(forkHost, errors)); | |||
validateName(request, errors); | |||
@@ -146,11 +169,8 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
} | |||
} else { | |||
if (!request.hasNickname()) { | |||
if (request.hasName()) { | |||
request.setNickname(request.getName()); | |||
} else { | |||
errors.addViolation("err.name.required"); | |||
} | |||
if (!request.hasName()) request.setName(newNetworkName()); | |||
request.setNickname(request.getName()); | |||
} | |||
if (request.hasNickname() && request.getNickname().length() > NAME_MAXLEN) { | |||
errors.addViolation("err.name.tooLong"); | |||
@@ -160,9 +180,14 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
} | |||
log.info("setReferences: after calling validateName, request.name="+request.getName()); | |||
final BubblePlan plan = planDAO.findByAccountOrParentAndId(caller, request.getPlan()); | |||
BubblePlan plan = planDAO.findByAccountOrParentAndId(caller, request.getPlan()); | |||
if (plan == null) { | |||
errors.addViolation("err.plan.required"); | |||
plan = planDAO.findByUuid(caller.getPreferredPlan()); | |||
if (plan == null) { | |||
errors.addViolation("err.plan.required"); | |||
} else { | |||
request.setPlan(plan.getUuid()); | |||
} | |||
} else { | |||
request.setPlan(plan.getUuid()); | |||
} | |||
@@ -189,22 +214,26 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
AccountPaymentMethod paymentMethod = null; | |||
if (configuration.paymentsEnabled()) { | |||
if (!request.hasPaymentMethodObject()) { | |||
errors.addViolation("err.paymentMethod.required"); | |||
} else { | |||
if (request.getPaymentMethodObject().hasUuid()) { | |||
paymentMethod = paymentMethodDAO.findByUuid(request.getPaymentMethodObject().getUuid()); | |||
if (paymentMethod == null) errors.addViolation("err.purchase.paymentMethodNotFound"); | |||
final List<AccountPaymentMethod> paymentMethods = paymentMethodDAO.findByAccountAndNotPromoAndNotDeleted(caller.getUuid()); | |||
if (empty(paymentMethods)) { | |||
errors.addViolation("err.paymentMethod.required"); | |||
} else { | |||
paymentMethod = request.getPaymentMethodObject(); | |||
request.setPaymentMethodObject(paymentMethods.get(0)); | |||
} | |||
if (paymentMethod != null && plan != null) { | |||
if (paymentMethod.hasPromotion() || paymentMethod.getPaymentMethodType() == PaymentMethodType.promotional_credit) { | |||
// cannot pay with a promo credit, must supply another payment method. | |||
// promos will be applied at purchase, and may result in no charge to this payment method | |||
errors.addViolation("err.purchase.paymentMethodNotFound"); | |||
} else { | |||
paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration); | |||
} | |||
} | |||
if (request.getPaymentMethodObject().hasUuid()) { | |||
paymentMethod = paymentMethodDAO.findByUuid(request.getPaymentMethodObject().getUuid()); | |||
if (paymentMethod == null) errors.addViolation("err.purchase.paymentMethodNotFound"); | |||
} else { | |||
paymentMethod = request.getPaymentMethodObject(); | |||
} | |||
if (paymentMethod != null && plan != null) { | |||
if (paymentMethod.hasPromotion() || paymentMethod.getPaymentMethodType() == PaymentMethodType.promotional_credit) { | |||
// cannot pay with a promo credit, must supply another payment method. | |||
// promos will be applied at purchase, and may result in no charge to this payment method | |||
errors.addViolation("err.purchase.paymentMethodNotFound"); | |||
} else { | |||
paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration); | |||
} | |||
} | |||
} | |||
@@ -7,6 +7,7 @@ package bubble.service.cloud; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.notify.NewNodeNotification; | |||
import bubble.server.BubbleConfiguration; | |||
import lombok.AllArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -25,6 +26,7 @@ public class NodeLauncher implements Runnable { | |||
private final AtomicReference<String> lock; | |||
private final StandardNetworkService networkService; | |||
private final NodeLaunchMonitor launchMonitor; | |||
private final BubbleConfiguration configuration; | |||
@Override public void run() { | |||
final String networkUuid = newNodeRequest.getNetwork(); | |||
@@ -98,7 +100,7 @@ public class NodeLauncher implements Runnable { | |||
die("NodeLauncher.run: unknown launch exception (type="+launchException.getType()+"): "+shortError(launchException), launchException); | |||
} | |||
} else { | |||
die("NodeLauncher.run: fatal launch exception: " + shortError(exception), exception); | |||
if (!configuration.testMode()) die("NodeLauncher.run: fatal launch exception: " + shortError(exception), exception); | |||
} | |||
} | |||
if (node != null && node.isRunning()) { | |||
@@ -817,7 +817,7 @@ public class StandardNetworkService implements NetworkService { | |||
public void backgroundNewNode(NewNodeNotification newNodeRequest, final String existingLock) { | |||
final AtomicReference<String> lock = new AtomicReference<>(existingLock); | |||
daemon(new NodeLauncher(newNodeRequest, lock, this, launchMonitor)); | |||
daemon(new NodeLauncher(newNodeRequest, lock, this, launchMonitor, configuration)); | |||
} | |||
public boolean stopNetwork(final BubbleNetwork network) { | |||
@@ -1 +1 @@ | |||
Subproject commit 35f36a84d84cb1f73dd8cc1f18e044657675b7f9 | |||
Subproject commit 7ddb8f71fc43a5b5ce1056944e1f1876ee586f14 |
@@ -67,6 +67,12 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { | |||
super.beforeStart(server); | |||
} | |||
public void mockNetwork(RestServer<BubbleConfiguration> server) { | |||
final BubbleConfiguration configuration = server.getConfiguration(); | |||
configuration.setSpringContextPath("classpath:/spring-mock-network.xml"); | |||
configuration.getStaticAssets().setLocalOverride(null); | |||
} | |||
public String getDefaultDomain() { return "example.com"; } | |||
@Override protected String[] getSqlPostScripts() { return hasExistingDb ? null : super.getSqlPostScripts(); } | |||
@@ -15,9 +15,7 @@ public class PaymentTestBase extends ActivatedBubbleModelTestBase { | |||
@Override protected String getManifest() { return "manifest-test"; } | |||
@Override public void beforeStart(RestServer<BubbleConfiguration> server) { | |||
final BubbleConfiguration configuration = server.getConfiguration(); | |||
configuration.setSpringContextPath("classpath:/spring-mock-network.xml"); | |||
configuration.getStaticAssets().setLocalOverride(null); | |||
mockNetwork(server); | |||
super.beforeStart(server); | |||
} | |||
@@ -12,5 +12,6 @@ public class NetworkTest extends NetworkTestBase { | |||
@Test public void testRegions () throws Exception { modelTest("network/network_regions"); } | |||
@Test public void testGetNetworkKeys () throws Exception { modelTest("network/network_keys"); } | |||
@Test public void testMinimalLaunch () throws Exception { modelTest("network/minimal_launch"); } | |||
} |
@@ -4,10 +4,17 @@ | |||
*/ | |||
package bubble.test.system; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.test.ActivatedBubbleModelTestBase; | |||
import org.cobbzilla.wizard.server.RestServer; | |||
public class NetworkTestBase extends ActivatedBubbleModelTestBase { | |||
@Override protected String getManifest() { return "manifest-network"; } | |||
@Override public void beforeStart(RestServer<BubbleConfiguration> server) { | |||
mockNetwork(server); | |||
super.beforeStart(server); | |||
} | |||
} |
@@ -0,0 +1,69 @@ | |||
[ | |||
{ | |||
"comment": "create another account and login", | |||
"include": "new_account", | |||
"params": { | |||
"email": "min_launch@example.com", | |||
"verifyEmail": true | |||
} | |||
}, | |||
{ | |||
"comment": "list plans", | |||
"request": { "uri": "plans" }, | |||
"response": { | |||
"store": "plans", | |||
"check": [ | |||
{"condition": "json.length > 0"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "add a payment method and preferred plan", | |||
"before": "stripe_tokenize_card", | |||
"request": { | |||
"uri": "me/paymentMethods", | |||
"method": "put", | |||
"entity": { | |||
"paymentMethodType": "credit", | |||
"paymentInfo": "{{stripeToken}}", | |||
"preferredPlan": "{{plans.[0].name}}" | |||
} | |||
} | |||
}, | |||
{ | |||
"comment": "add account plan", | |||
"request": { | |||
"uri": "me/plans", | |||
"method": "put", | |||
"entity": {} | |||
}, | |||
"response": { "store": "plan" } | |||
}, | |||
{ | |||
"comment": "start the network. sets up the first node, which does the rest", | |||
"request": { | |||
"uri": "me/networks/{{ plan.name }}/actions/start", | |||
"method": "post" | |||
}, | |||
"response": { | |||
"store": "<<networkVar>>" | |||
} | |||
}, | |||
{ | |||
"comment": "list networks, verify new network is starting", | |||
"before": "sleep 10s", | |||
"request": { "uri": "me/networks" }, | |||
"response": { | |||
"check": [ | |||
{"condition": "json.length === 1"}, | |||
{"condition": "json[0].getName() === '{{plan.name}}'"}, | |||
{"condition": "json[0].getState().name() === 'starting'"} | |||
] | |||
} | |||
} | |||
] |