@@ -206,6 +206,9 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
final Map<String, BubbleApp> apps = new HashMap<>(); | |||
copyTemplateObjects(acct, parent, appDAO, new AccountTemplate.CopyTemplate<>() { | |||
@Override public BubbleApp preCreate(BubbleApp parentApp, BubbleApp accountApp) { | |||
return accountApp.setTemplateApp(parentApp.getUuid()); | |||
} | |||
@Override public void postCreate(BubbleApp parentApp, BubbleApp accountApp) { | |||
apps.put(parentApp.getUuid(), accountApp); | |||
} | |||
@@ -25,4 +25,8 @@ public class BubbleAppDAO extends AccountOwnedTemplateDAO<BubbleApp> { | |||
super.delete(uuid); | |||
} | |||
public BubbleApp findByAccountAndTemplateApp(String accountUuid, String templateAppUuid) { | |||
return findByUniqueFields("account", accountUuid, "templateApp", templateAppUuid); | |||
} | |||
} |
@@ -3,6 +3,7 @@ package bubble.dao.bill; | |||
import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.dao.app.BubbleAppDAO; | |||
import bubble.model.app.BubbleApp; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.model.bill.BubblePlanApp; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
@@ -16,16 +17,16 @@ public class BubblePlanAppDAO extends AccountOwnedEntityDAO<BubblePlanApp> { | |||
@Override public boolean dbFilterIncludeAll() { return true; } | |||
public List<BubblePlanApp> findByPlan(String bubblePlan) { | |||
return findByField("plan", bubblePlan); | |||
public List<BubblePlanApp> findByPlan(String bubblePlanUuid) { | |||
return findByField("plan", bubblePlanUuid); | |||
} | |||
public BubblePlanApp findByAccountAndPlanAndId(String account, String bubblePlan, String id) { | |||
final BubblePlanApp planApp = findByUniqueFields("plan", bubblePlan, "app", id); | |||
public BubblePlanApp findByPlanAndId(BubblePlan plan, String id) { | |||
final BubblePlanApp planApp = findByUniqueFields("plan", plan.getUuid(), "app", id); | |||
if (planApp != null) return planApp; | |||
final BubbleApp app = appDAO.findByAccountAndId(account, id); | |||
return app == null ? null : findByUniqueFields("plan", bubblePlan, "app", app.getUuid()); | |||
final BubbleApp app = appDAO.findByAccountAndId(plan.getAccount(), id); | |||
return app == null ? null : findByUniqueFields("plan", plan.getUuid(), "app", app.getUuid()); | |||
} | |||
} |
@@ -30,4 +30,12 @@ public class BubblePlanDAO extends AccountOwnedEntityDAO<BubblePlan> { | |||
return null; | |||
} | |||
public BubblePlan findByName(String name) { return findByUniqueField("name", name); } | |||
public BubblePlan findById(String id) { | |||
final BubblePlan plan = findByUuid(id); | |||
if (plan != null) return plan; | |||
return findByName(id); | |||
} | |||
} |
@@ -115,7 +115,8 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
// make this updatable if we ever want accounts to be able to change parents | |||
// there might be a lot more involved in that action though (read-only parent objects that will no longer be visible, must be copied in?) | |||
@ECIndex @Column(length=UUID_MAXLEN, updatable=false) @ECField(index=20, mode=EntityFieldMode.readOnly) | |||
@ECForeignKey(entity=Account.class) @ECField(index=20, mode=EntityFieldMode.readOnly) | |||
@Column(length=UUID_MAXLEN, updatable=false) | |||
@Getter @Setter private String parent; | |||
public boolean hasParent () { return parent != null; } | |||
@@ -39,6 +39,7 @@ import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; | |||
@Entity @NoArgsConstructor @Accessors(chain=true) | |||
@ECIndexes({ | |||
@ECIndex(unique=true, of={"account", "name"}), | |||
@ECIndex(unique=true, of={"account", "templateApp"}), | |||
@ECIndex(of={"account", "template", "enabled"}), | |||
@ECIndex(of={"template", "enabled"}) | |||
}) | |||
@@ -93,15 +94,23 @@ public class BubbleApp extends IdentifiableBaseParentEntity implements AccountTe | |||
return adc; | |||
} | |||
@ECSearchable @ECField(index=60) | |||
// We do NOT add @ECForeignKey here, since the template BubbleApp will not be copied | |||
// to a new node. This App will become a template/root BubbleApp for a new node, if it | |||
// is owned by a user and applicable to the BubblePlan (via BubblePlanApp) | |||
// For system apps, this can be null | |||
@ECField(index=60) | |||
@Column(length=UUID_MAXLEN, updatable=false) | |||
@Getter @Setter private String templateApp; | |||
@ECSearchable @ECField(index=70) | |||
@ECIndex @Column(nullable=false) | |||
@Getter @Setter private Boolean template = false; | |||
@ECSearchable @ECField(index=70) | |||
@ECSearchable @ECField(index=80) | |||
@ECIndex @Column(nullable=false) | |||
@Getter @Setter private Boolean enabled = true; | |||
@ECSearchable @ECField(index=80) | |||
@ECSearchable @ECField(index=90) | |||
@ECIndex @Getter @Setter private Boolean needsUpdate = false; | |||
} |
@@ -35,7 +35,6 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
@ECTypeChild(type=BubblePlanApp.class, backref="plan") | |||
}) | |||
@Entity @NoArgsConstructor @Accessors(chain=true) | |||
@ECIndexes({ @ECIndex(unique=true, of={"account", "name"}) }) | |||
public class BubblePlan extends IdentifiableBaseParentEntity implements HasAccount, HasPriority { | |||
public static final int MAX_CHARGENAME_LEN = 12; | |||
@@ -55,7 +54,7 @@ public class BubblePlan extends IdentifiableBaseParentEntity implements HasAccou | |||
@ECSearchable(filter=true) @ECField(index=10) | |||
@HasValue(message="err.name.required") | |||
@ECIndex @Column(nullable=false, updatable=false, length=200) | |||
@ECIndex(unique=true) @Column(nullable=false, updatable=false, length=200) | |||
@Getter @Setter private String name; | |||
@ECSearchable @ECField(index=20) | |||
@@ -13,6 +13,7 @@ import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
import javax.persistence.Column; | |||
import javax.persistence.Entity; | |||
import javax.persistence.Transient; | |||
import static bubble.ApiConstants.EP_APPS; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
@@ -47,4 +48,6 @@ public class BubblePlanApp extends IdentifiableBase implements HasAccountNoName | |||
@Column(nullable=false, updatable=false, length=UUID_MAXLEN) | |||
@Getter @Setter private String app; | |||
@Transient @Getter @Setter private transient BubbleApp appObject; | |||
} |
@@ -28,12 +28,19 @@ public class BubblePlanAppsResource extends AccountOwnedResource<BubblePlanApp, | |||
@Autowired private BubbleAppDAO appDAO; | |||
@Override protected List<BubblePlanApp> list(ContainerRequest ctx) { | |||
return getDao().findByPlan(plan.getUuid()); | |||
} | |||
@Override protected List<BubblePlanApp> list(ContainerRequest ctx) { return getDao().findByPlan(plan.getUuid()); } | |||
@Override protected BubblePlanApp find(ContainerRequest ctx, String id) { return getDao().findByPlanAndId(plan, id); } | |||
@Override protected BubblePlanApp find(ContainerRequest ctx, String id) { | |||
return getDao().findByAccountAndPlanAndId(account.getUuid(), plan.getUuid(), id); | |||
@Override protected BubblePlanApp populate(ContainerRequest ctx, BubblePlanApp planApp) { | |||
final BubbleApp globalApp = appDAO.findByAccountAndId(planApp.getAccount(), planApp.getApp()); | |||
if (globalApp == null) { | |||
log.warn("populate: globalApp "+planApp.getApp()+" not found for planApp: "+planApp.getUuid()); | |||
} else { | |||
final BubbleApp userApp = appDAO.findByAccountAndTemplateApp(getAccountUuid(ctx), globalApp.getUuid()); | |||
planApp.setAppObject(userApp); | |||
} | |||
return super.populate(ctx, planApp); | |||
} | |||
@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, BubblePlanApp request) { | |||
@@ -10,6 +10,7 @@ import bubble.model.bill.BubblePlanApp; | |||
import bubble.resources.account.AccountOwnedResource; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.collection.ExpirationMap; | |||
import org.glassfish.grizzly.http.server.Request; | |||
import org.glassfish.jersey.server.ContainerRequest; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
@@ -42,6 +43,31 @@ public class BubblePlansResource extends AccountOwnedResource<BubblePlan, Bubble | |||
return super.setReferences(ctx, caller, bubblePlan); | |||
} | |||
// BubblePlan objects are global, no need to qualify by account | |||
@Override protected BubblePlan find(ContainerRequest ctx, String id) { return getDao().findById(id); } | |||
@Override protected List<BubblePlan> list(ContainerRequest ctx) { return getDao().findAll(); } | |||
// only admins can create | |||
@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, BubblePlan request) { | |||
return caller.admin(); | |||
} | |||
// only owner can edit | |||
@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, BubblePlan found, BubblePlan request) { | |||
return caller.admin() && caller.getUuid().equals(found.getAccount()); | |||
} | |||
// only owner can delete | |||
@Override protected boolean canDelete(ContainerRequest ctx, Account caller, BubblePlan found) { | |||
return caller.admin() && caller.getUuid().equals(found.getAccount()); | |||
} | |||
@Override protected BubblePlan populate(ContainerRequest ctx, BubblePlan plan) { | |||
final List<BubbleApp> apps = getAppsForPlan(plan); | |||
plan.setApps(apps); | |||
return super.populate(ctx, plan); | |||
} | |||
@Path("/{id}"+EP_APPS) | |||
public BubblePlanAppsResource getApps(@Context ContainerRequest ctx, | |||
@PathParam("id") String id) { | |||
@@ -51,12 +77,6 @@ public class BubblePlansResource extends AccountOwnedResource<BubblePlan, Bubble | |||
return configuration.subResource(BubblePlanAppsResource.class, caller, plan); | |||
} | |||
@Override protected BubblePlan populate(ContainerRequest ctx, BubblePlan plan) { | |||
final List<BubbleApp> apps = getAppsForPlan(plan); | |||
plan.setApps(apps); | |||
return super.populate(ctx, plan); | |||
} | |||
private Map<String, List<BubbleApp>> appCache = new ExpirationMap<>(); | |||
private List<BubbleApp> getAppsForPlan(BubblePlan plan) { | |||
@@ -10,6 +10,7 @@ import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.CloudService; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.model.Identifiable; | |||
import java.util.Iterator; | |||
@@ -23,7 +24,9 @@ import static bubble.cloud.storage.local.LocalStorageDriver.LOCAL_STORAGE_STANDA | |||
import static bubble.service.dbfilter.EndOfEntityStream.END_OF_ENTITY_STREAM; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.wizard.model.NamedEntity.names; | |||
@Slf4j | |||
public abstract class EntityIterator implements Iterator<Identifiable> { | |||
private static final int MAX_QUEUE_SIZE = 100; | |||
@@ -32,6 +35,7 @@ public abstract class EntityIterator implements Iterator<Identifiable> { | |||
@Getter private final Thread thread; | |||
@Getter private final AtomicReference<Exception> error; | |||
private List<BubbleApp> userApps; | |||
public EntityIterator(AtomicReference<Exception> error) { | |||
this.error = error; | |||
@@ -84,14 +88,46 @@ public abstract class EntityIterator implements Iterator<Identifiable> { | |||
entities.forEach(e -> add(setInstallKey((AccountSshKey) e, network))); | |||
} else if (planApps != null && BubbleApp.class.isAssignableFrom(c)) { | |||
// only copy enabled apps | |||
// only copy enabled apps, make them templates | |||
entities.stream().filter(e -> planAppEnabled(e.getUuid(), planApps)) | |||
.map(app -> ((BubbleApp) app).setTemplate(true)) | |||
.forEach(this::add); | |||
// save these for later, we will need them when copying BubblePlanApps below | |||
userApps = (List<BubbleApp>) entities; | |||
} else if (planApps != null && BubblePlanApp.class.isAssignableFrom(c)) { | |||
// the only BubblePlanApps we will see here are the ones associated with the system BubblePlans | |||
// and the system/template BubbleApps. | |||
// But for this new node, the BubbleApps that are associated with the first user (admin of new node) | |||
// will become the new system/template apps. | |||
// So we rewrite the "app" field to refer to the BubbleApp owned by the user. | |||
// Unless for some odd reason we are deploying a node with NO apps, in which case we can skip this section entirely | |||
if (planApps.isEmpty()) { | |||
log.warn("addEntities: no BubblePlanApps enabled, none will be copied to new node"); | |||
} else { | |||
for (Identifiable e : entities) { | |||
final BubblePlanApp systemPlanApp = (BubblePlanApp) e; | |||
final BubbleApp userApp = userApps.stream() | |||
.filter(app -> app.getTemplateApp().equals(systemPlanApp.getApp())) | |||
.findFirst().orElse(null); | |||
if (userApp == null) { | |||
log.info("addEntities: system BubblePlanApp " + systemPlanApp.getName() + " not found in userApps (not adding): " + names(userApps)); | |||
} else { | |||
// systemPlanApp will now be associated with "root"'s BubblePlan, but user's BubbleApp | |||
log.info("addEntities: rewrite "); | |||
systemPlanApp.setApp(userApp.getUuid()); | |||
} | |||
} | |||
} | |||
} else if (planApps != null && AppTemplateEntity.class.isAssignableFrom(c)) { | |||
// only copy app-related entities for enabled apps | |||
// only copy app-related entities for enabled apps, make them all templates | |||
entities.stream() | |||
.filter(e -> planAppEnabled(((AppTemplateEntity) e).getApp(), planApps)) | |||
.map(app -> (AppTemplateEntity) ((AppTemplateEntity) app).setTemplate(true)) | |||
.forEach(this::add); | |||
} else { | |||
@@ -35,7 +35,25 @@ | |||
"request": { "uri": "plans" }, | |||
"response": { | |||
"store": "plans", | |||
"check": [{"condition": "json.length >= 1"}] | |||
"check": [ | |||
{"condition": "json.length >= 1"}, | |||
{"condition": "json[0].getApps().length >= 1"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "get plan apps for a system plan, we actually see our apps underneath", | |||
"request": { "uri": "plans/{{plans.[0].name}}/apps" }, | |||
"response": { | |||
"store": "planApps", | |||
"check": [ | |||
{"condition": "json.length === 1"}, | |||
{"condition": "json[0].getAppObject().getTemplateApp() !== null"}, | |||
{"condition": "json[0].getAppObject().template() === false"}, | |||
{"condition": "json[0].getAppObject().getUuid() !== json[0].getApp()"}, | |||
{"condition": "json[0].getAppObject().getTemplateApp() === json[0].getApp()"} | |||
] | |||
} | |||
}, | |||
@@ -63,10 +81,17 @@ | |||
}, | |||
{ | |||
"comment": "get plan apps, should be 1", | |||
"comment": "get plan apps, should be 1 and should have templateApp", | |||
"request": { "uri": "me/plans/{{accountPlan.uuid}}/apps" }, | |||
"response": { | |||
"check": [{"condition": "json.length == 1"}] | |||
"check": [ | |||
{"condition": "json.length === 1"}, | |||
{"condition": "json[0].getAppObject().getTemplateApp() !== null"}, | |||
{"condition": "json[0].getAppObject().getName() === 'UserBlocker'"}, | |||
{"condition": "json[0].getAppObject().getUuid() !== json[0].getApp()"}, | |||
{"condition": "json[0].getAppObject().getTemplateApp() === json[0].getApp()"}, | |||
{"condition": "json[0].getAppObject().template() === false"} | |||
] | |||
} | |||
}, | |||
@@ -97,7 +122,7 @@ | |||
"comment": "get plan apps, should be 2", | |||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/apps" }, | |||
"response": { | |||
"check": [{"condition": "json.length == 2"}] | |||
"check": [{"condition": "json.length === 2"}] | |||
} | |||
} | |||
@@ -1 +1 @@ | |||
Subproject commit 88f19d61844a4fd7bce11f81021e798473b9f135 | |||
Subproject commit 7ce51555339f93ec769066bf66b1a2831f298b22 |