diff --git a/.gitignore b/.gitignore index c69d4b9a..219f993a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ target .DS_Store .BUBBLE* +configs diff --git a/bin/activate b/bin/activate deleted file mode 100755 index 4d350ac5..00000000 --- a/bin/activate +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# -# Initial activation of a bubble server -# -# Usage: activate [domain] [dns] [storage] -# -# domain : a domain name. Must be listed in bubble-server/src/test/resources/models/system/bubbleDomain.json -# default value is bubblev.org -# dns : name of a CloudService of type 'dns'. Must be listed in bubble-server/src/test/resources/models/system/cloudService.json -# default is GoDaddyDns -# storage : name of a CloudService of type 'storage'. Must be listed in cloudService.json -# default is S3_US_Standard -# -# Environment variables -# -# BUBBLE_API : which API to use. Default is local (http://127.0.0.1:PORT, where PORT is found in .bubble.env) -# BUBBLE_USER : account to use. Default is root -# BUBBLE_PASS : password for account. Default is root -# -SCRIPT="${0}" -SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) -. ${SCRIPT_DIR}/bubble_common - -if [[ -z "${BUBBLE_JAR}" ]] ; then - die "BUBBLE_JAR env var not set and no jar file found" -fi - -MODELS_DIR="${SCRIPT_DIR}/../bubble-server/src/test/resources/models/system" -if [[ ! -d ${MODELS_DIR} ]] ; then - die "Models directory not found: ${MODELS_DIR}" -fi - -ENV_FILE="${HOME}/.bubble.env" -if [[ ! -f ${ENV_FILE} ]] ; then - die "env file not found: ${ENV_FILE}" -fi - -# Source env vars -. ${ENV_FILE} - -DOMAIN=${1:-bubblev.org} -DOMAINS_FILE="${MODELS_DIR}/bubbleDomain.json" -DOMAIN_JSON=$(cat ${DOMAINS_FILE} | sed -e 's,// .*,,g' | grep -v "_subst" | java -cp ${BUBBLE_JAR} bubble.main.BubbleMain handlebars | jq ".[] | select(.name==\"${DOMAIN}\")") -if [[ -z "${DOMAIN_JSON}" ]] ; then - die "Domain ${DOMAIN} not found in ${DOMAINS_FILE}" -fi - -DNS_CLOUD=${2:-GoDaddyDns} -CLOUDS_FILE="${MODELS_DIR}/cloudService.json" -DNS_JSON=$(cat ${CLOUDS_FILE} | sed -e 's,// .*,,g' | grep -v "_subst" | java -cp ${BUBBLE_JAR} bubble.main.BubbleMain handlebars | jq ".[] | select(.name==\"${DNS_CLOUD}\")") -if [[ -z "${DNS_JSON}" ]] ; then - die "DNS CloudService ${DNS_CLOUD} not found in ${CLOUDS_FILE}" -fi -CLOUD_TYPE="$(echo ${DNS_JSON} | jq -r .type)" -if [[ -z "${CLOUD_TYPE}" ]] ; then - die "DNS service ${DNS_CLOUD} has no type" -fi -if [[ "${CLOUD_TYPE}" != 'dns' ]] ; then - die "DNS service ${DNS_CLOUD} has wrong type (${CLOUD_TYPE}), expected: dns" -fi - -STORAGE_CLOUD=${2:-S3_US_Standard} -STORAGE_JSON=$(cat ${CLOUDS_FILE} | sed -e 's,// .*,,g' | grep -v "_subst" | java -cp ${BUBBLE_JAR} bubble.main.BubbleMain handlebars | jq ".[] | select(.name==\"${STORAGE_CLOUD}\")") -if [[ -z "${STORAGE_JSON}" ]] ; then - die "Storage CloudService ${STORAGE_CLOUD} not found in ${CLOUDS_FILE}" -fi -CLOUD_TYPE="$(echo ${STORAGE_JSON} | jq -r .type)" -if [[ -z "${CLOUD_TYPE}" ]] ; then - die "Storage service ${DNS_CLOUD} has no type" -fi -if [[ "${CLOUD_TYPE}" != 'storage' ]] ; then - die "Storage service ${DNS_CLOUD} has wrong type (${CLOUD_TYPE}), expected: storage" -fi - -exec ${SCRIPT_DIR}/bput auth/activate - --no-login < T setupDriver(BubbleConfiguration configuration, T driver) { diff --git a/bubble-server/src/main/java/bubble/cloud/compute/ComputeServiceDriverBase.java b/bubble-server/src/main/java/bubble/cloud/compute/ComputeServiceDriverBase.java index 4f227975..a7101678 100644 --- a/bubble-server/src/main/java/bubble/cloud/compute/ComputeServiceDriverBase.java +++ b/bubble-server/src/main/java/bubble/cloud/compute/ComputeServiceDriverBase.java @@ -25,7 +25,7 @@ public abstract class ComputeServiceDriverBase private final AtomicReference reaper = new AtomicReference<>(); - @Override public void startDriver() { + @Override public void postSetup() { if (configuration.isSelfSage()) { synchronized (reaper) { if (reaper.get() == null) { diff --git a/bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java b/bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java index 9ea4b600..219e90f7 100644 --- a/bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java @@ -78,6 +78,7 @@ public class VultrDriver extends ComputeServiceDriverBase { final String apiKey = HandlebarsUtil.apply(getHandlebars(), credentials.getParam(API_KEY_HEADER), configuration.getEnvCtx()); credentials.setParam(API_KEY_HEADER, apiKey); } + super.postSetup(); } @Override public BubbleNode start(BubbleNode node) throws Exception { diff --git a/bubble-server/src/main/java/bubble/cloud/dns/DnsServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/dns/DnsServiceDriver.java index 84637de2..a3179c1e 100644 --- a/bubble-server/src/main/java/bubble/cloud/dns/DnsServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/dns/DnsServiceDriver.java @@ -48,7 +48,7 @@ public interface DnsServiceDriver extends CloudServiceDriver { default Collection list() { return list(null); } - @Override default boolean test(Object arg) { return !empty(list((DnsRecordMatch) arg)); } + @Override default boolean test(Object arg) { return arg == null ? test() : !empty(list((DnsRecordMatch) arg)); } @Override default boolean test() { return true; } default Collection listNew(Long lastMod) { return list(); } diff --git a/bubble-server/src/main/java/bubble/cloud/dns/godaddy/GoDaddyDnsDriver.java b/bubble-server/src/main/java/bubble/cloud/dns/godaddy/GoDaddyDnsDriver.java index da61039d..7c661b8b 100644 --- a/bubble-server/src/main/java/bubble/cloud/dns/godaddy/GoDaddyDnsDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/dns/godaddy/GoDaddyDnsDriver.java @@ -19,8 +19,7 @@ import java.util.stream.Collectors; import static java.util.Collections.emptyList; import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.http.HttpHeaders.CONTENT_TYPE; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.retry; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.dns.DnsType.NS; import static org.cobbzilla.util.dns.DnsType.SOA; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; @@ -143,6 +142,15 @@ public class GoDaddyDnsDriver extends DnsDriverBase { } } + @Override public boolean test() { + final String url = config.getBaseUri(); + try { + return HttpUtil.getResponse(auth(url)).isOk(); + } catch (Exception e) { + return die("test: "+shortError(e)); + } + } + @Override public Collection list(DnsRecordMatch matcher) { final BubbleDomain domain = getDomain(matcher); diff --git a/bubble-server/src/main/java/bubble/cloud/dns/route53/Route53DnsDriver.java b/bubble-server/src/main/java/bubble/cloud/dns/route53/Route53DnsDriver.java index 66d798f7..8df04b56 100644 --- a/bubble-server/src/main/java/bubble/cloud/dns/route53/Route53DnsDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/dns/route53/Route53DnsDriver.java @@ -45,10 +45,10 @@ public class Route53DnsDriver extends DnsDriverBase { } @Getter(lazy=true) private final Map cachedZoneLookups = new ExpirationMap<>(); - private HostedZone getHostedZone(AmazonRoute53 client, BubbleDomain domain) { + private HostedZone getHostedZone(BubbleDomain domain) { return getCachedZoneLookups().computeIfAbsent(domain.getName(), key -> { try { - final ListHostedZonesResult zones = client.listHostedZones(new ListHostedZonesRequest().withMaxItems("100")); + final ListHostedZonesResult zones = getRoute53client().listHostedZones(new ListHostedZonesRequest().withMaxItems("100")); for (HostedZone z : zones.getHostedZones()) { if (z.getName().equalsIgnoreCase(key + ".")) return z; } @@ -59,10 +59,15 @@ public class Route53DnsDriver extends DnsDriverBase { }); } + @Override public boolean test() { + getRoute53client().listHostedZones(new ListHostedZonesRequest().withMaxItems("10")); + return true; + } + @Override public Collection create(BubbleDomain domain) { final AmazonRoute53 client = getRoute53client(); - final HostedZone hostedZone = getHostedZone(client, domain); + final HostedZone hostedZone = getHostedZone(domain); final ListResourceRecordSetsResult soaRecords = client.listResourceRecordSets(new ListResourceRecordSetsRequest() .withHostedZoneId(hostedZone.getId()) @@ -144,7 +149,7 @@ public class Route53DnsDriver extends DnsDriverBase { @Override public DnsRecord update(DnsRecord record) { final AmazonRoute53 client = getRoute53client(); final BubbleDomain domain = getDomain(record.getFqdn()); - final HostedZone hostedZone = getHostedZone(client, domain); + final HostedZone hostedZone = getHostedZone(domain); final ChangeResourceRecordSetsResult changeResult = client.changeResourceRecordSets(new ChangeResourceRecordSetsRequest() .withHostedZoneId(hostedZone.getId()) .withChangeBatch(new ChangeBatch().withChanges(new Change() @@ -159,7 +164,7 @@ public class Route53DnsDriver extends DnsDriverBase { @Override public DnsRecord remove(DnsRecord record) { final AmazonRoute53 client = getRoute53client(); final BubbleDomain domain = getDomain(record.getFqdn()); - final HostedZone hostedZone = getHostedZone(client, domain); + final HostedZone hostedZone = getHostedZone(domain); final ListResourceRecordSetsRequest listRequest = new ListResourceRecordSetsRequest() .withHostedZoneId(hostedZone.getId()) @@ -186,11 +191,11 @@ public class Route53DnsDriver extends DnsDriverBase { } @Override public Collection list(DnsRecordMatch matcher) { - final BubbleDomain domain = getDomain(matcher.getFqdn()); + final BubbleDomain domain = getDomain(matcher); if (domain == null) return emptyList(); final AmazonRoute53 client = getRoute53client(); - final HostedZone hostedZone = getHostedZone(client, domain); + final HostedZone hostedZone = getHostedZone(domain); final ListResourceRecordSetsRequest listRequest = new ListResourceRecordSetsRequest() .withHostedZoneId(hostedZone.getId()) .withStartRecordName(matcher.hasFqdn() ? matcher.getFqdn() : domain.getName()) diff --git a/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeServiceDriver.java index 537311eb..36059c48 100644 --- a/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeServiceDriver.java @@ -9,7 +9,7 @@ public interface GeoTimeServiceDriver extends CloudServiceDriver { String TEST_LONGITUDE = "-73.944"; String TEST_LATITUDE = "40.661"; String TEST_STANDARD_NAME = "Eastern Standard Time"; - String TEST_TIMEZONE_ID = "America/New York"; + String TEST_TIMEZONE_ID = "America/New_York"; @Override default CloudServiceType getType() { return CloudServiceType.geoTime; } diff --git a/bubble-server/src/main/java/bubble/cloud/sms/twilio/TwilioSmsDriver.java b/bubble-server/src/main/java/bubble/cloud/sms/twilio/TwilioSmsDriver.java index 7b0bf104..f96514e9 100644 --- a/bubble-server/src/main/java/bubble/cloud/sms/twilio/TwilioSmsDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/sms/twilio/TwilioSmsDriver.java @@ -25,7 +25,7 @@ public class TwilioSmsDriver extends SmsServiceDriverBase { @Autowired @Getter protected BubbleConfiguration configuration; - @Override public void startDriver() { + @Override public void postSetup() { synchronized (twilioInitDone) { if (!twilioInitDone.get()) { sid = getCredentials().getParam(PARAM_ACCOUNT_SID); diff --git a/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriverBase.java b/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriverBase.java index 97efece8..b77197c7 100644 --- a/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriverBase.java +++ b/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriverBase.java @@ -19,7 +19,7 @@ public abstract class StorageServiceDriverBase extends CloudServiceDriverBase private static final Map cleaners = new ConcurrentHashMap<>(); - @Override public void startDriver() { + @Override public void postSetup() { final String key = sha256_hex(json(getConfig())+":"+json(getCredentials())); synchronized (cleaners) { WriteRequestCleaner cleaner = cleaners.get(key); diff --git a/bubble-server/src/main/java/bubble/cloud/storage/local/LocalStorageDriver.java b/bubble-server/src/main/java/bubble/cloud/storage/local/LocalStorageDriver.java index 4f1c3886..dbb18e25 100644 --- a/bubble-server/src/main/java/bubble/cloud/storage/local/LocalStorageDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/storage/local/LocalStorageDriver.java @@ -10,6 +10,7 @@ import bubble.model.cloud.CloudService; import bubble.model.cloud.StorageMetadata; import bubble.notify.storage.StorageListing; import lombok.Cleanup; +import lombok.Getter; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.cobbzilla.util.io.FileUtil; @@ -23,8 +24,7 @@ import java.util.List; import static bubble.ApiConstants.ROOT_NETWORK_UUID; import static bubble.dao.cloud.AnsibleRoleDAO.ROLE_PATH; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.io.FileUtil.*; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.security.ShaUtil.sha256_file; @@ -38,12 +38,30 @@ public class LocalStorageDriver extends CloudServiceDriverBase new File(file, s).isDirectory() && !UUID_PATTERN.matcher(s).find()); if (matched != null) { Arrays.stream(matched) .forEach(f -> { final String path = abs(f); - final String key = path.substring(config.getBaseDir().length()); - final File dest = new File(config.getBaseDir() + "/" + network.getUuid() + key); + final String key = path.substring(getBaseDir().length()); + final File dest = new File(getBaseDir() + "/" + network.getUuid() + key); try { FileUtils.copyDirectory(f, dest); } catch (IOException e) { diff --git a/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java b/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java index 83fd1da0..8722edf6 100644 --- a/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java +++ b/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java @@ -1,5 +1,6 @@ package bubble.dao.cloud; +import bubble.cloud.storage.local.LocalStorageDriver; import bubble.dao.account.AccountOwnedTemplateDAO; import bubble.model.cloud.CloudService; import bubble.cloud.CloudServiceType; @@ -8,11 +9,23 @@ import org.springframework.stereotype.Repository; import java.util.List; +import static bubble.cloud.storage.local.LocalStorageDriver.LOCAL_STORAGE; +import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; + @Repository public class CloudServiceDAO extends AccountOwnedTemplateDAO { @Override public Order getDefaultSortOrder() { return Order.desc("priority"); } + @Override public Object preCreate(CloudService cloud) { + if (cloud.getType() == CloudServiceType.storage + && cloud.getName().equals(LOCAL_STORAGE) + && !cloud.getDriver().getClass().equals(LocalStorageDriver.class)) { + throw invalidEx("err.cloud.localStorageIsReservedName"); + } + return super.preCreate(cloud); + } + @Override public CloudService postUpdate(CloudService cloud, Object context) { CloudService.clearDriverCache(cloud.getUuid()); return super.postUpdate(cloud, context); diff --git a/bubble-server/src/main/java/bubble/main/BubbleMain.java b/bubble-server/src/main/java/bubble/main/BubbleMain.java index ab5ee889..a73e4220 100644 --- a/bubble-server/src/main/java/bubble/main/BubbleMain.java +++ b/bubble-server/src/main/java/bubble/main/BubbleMain.java @@ -8,6 +8,7 @@ import bubble.server.BubbleServer; import org.cobbzilla.util.collection.MapBuilder; import org.cobbzilla.util.string.StringUtil; import org.cobbzilla.wizard.main.MainBase; +import org.slf4j.bridge.SLF4JBridgeHandler; import java.util.Map; import java.util.TreeSet; @@ -34,6 +35,9 @@ public class BubbleMain { if (args.length == 0) die(noCommandProvided()); + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + // extract command final String command = args[0]; final Class mainClass = mainClasses.get(command.toLowerCase()); diff --git a/bubble-server/src/main/java/bubble/main/http/BubbleHttpEntityOptions.java b/bubble-server/src/main/java/bubble/main/http/BubbleHttpEntityOptions.java index 0b73d6ac..53e0cbb2 100644 --- a/bubble-server/src/main/java/bubble/main/http/BubbleHttpEntityOptions.java +++ b/bubble-server/src/main/java/bubble/main/http/BubbleHttpEntityOptions.java @@ -1,16 +1,32 @@ package bubble.main.http; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.apache.http.entity.ContentType; import org.kohsuke.args4j.Option; import static org.cobbzilla.util.daemon.ZillaRuntime.readStdin; +import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; +import static org.cobbzilla.util.json.JsonUtil.*; +@Slf4j public class BubbleHttpEntityOptions extends BubbleHttpOptions { - public String getRequestJson() { return readStdin(); } + public String getRequestJson() { + final String data = readStdin(); + // does the JSON contain any comments? scrub them before sending... + if (data.contains("//") || data.contains("/*")) { + try { + return json(json(data, JsonNode.class, FULL_MAPPER_ALLOW_COMMENTS), COMPACT_MAPPER); + } catch (Exception e) { + log.warn("getRequestJson: error scrubbing comments from JSON, sending as-is: "+shortError(e)); + } + } + return data; + } public static final String USAGE_CONTENT_TYPE = "Content-Type to send. Default is application/json"; public static final String OPT_CONTENT_TYPE = "-C"; diff --git a/bubble-server/src/main/java/bubble/model/account/Account.java b/bubble-server/src/main/java/bubble/model/account/Account.java index 24b556f0..35d527be 100644 --- a/bubble-server/src/main/java/bubble/model/account/Account.java +++ b/bubble-server/src/main/java/bubble/model/account/Account.java @@ -3,6 +3,7 @@ package bubble.model.account; import bubble.dao.account.AccountInitializer; import bubble.model.app.AppData; import bubble.model.app.BubbleApp; +import bubble.model.boot.ActivationRequest; import bubble.model.cloud.*; import bubble.model.cloud.notify.SentNotification; import bubble.model.device.Device; diff --git a/bubble-server/src/main/java/bubble/model/account/ActivationRequest.java b/bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java similarity index 59% rename from bubble-server/src/main/java/bubble/model/account/ActivationRequest.java rename to bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java index 4b8204f5..e065e4d1 100644 --- a/bubble-server/src/main/java/bubble/model/account/ActivationRequest.java +++ b/bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java @@ -1,4 +1,4 @@ -package bubble.model.account; +package bubble.model.boot; import bubble.model.cloud.AnsibleRole; import bubble.model.cloud.BubbleDomain; @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.experimental.Accessors; import org.cobbzilla.wizard.validation.HasValue; +import java.util.LinkedHashMap; +import java.util.Map; + import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @NoArgsConstructor @Accessors(chain=true) @@ -26,18 +29,22 @@ public class ActivationRequest { public boolean hasDescription() { return !empty(description); } @HasValue(message="err.networkName.required") - @Getter @Setter private String networkName; + @Getter @Setter private String networkName = "boot-network"; @Getter @Setter private AnsibleRole[] roles; - public boolean hasRoles () { return roles != null && roles.length > 0; } + public boolean hasRoles () { return !empty(roles); } + + @Getter @Setter private Map cloudConfigs = new LinkedHashMap<>(); + public boolean hasCloudConfigs () { return !empty(cloudConfigs); } + public ActivationRequest addCloudConfig(CloudService cloud) { + cloudConfigs.put(cloud.getName(), cloud.toCloudConfig()); + return this; + } @HasValue(message="err.domain.required") @Getter @Setter private BubbleDomain domain; - @HasValue(message="err.dns.required") - @Getter @Setter private CloudService dns; - - @HasValue(message="err.storage.required") - @Getter @Setter private CloudService storage; + @Getter @Setter private Boolean createDefaultObjects = true; + public boolean createDefaultObjects () { return createDefaultObjects != null && createDefaultObjects; }; } diff --git a/bubble-server/src/main/java/bubble/model/boot/CloudServiceConfig.java b/bubble-server/src/main/java/bubble/model/boot/CloudServiceConfig.java new file mode 100644 index 00000000..5225de07 --- /dev/null +++ b/bubble-server/src/main/java/bubble/model/boot/CloudServiceConfig.java @@ -0,0 +1,34 @@ +package bubble.model.boot; + +import bubble.model.cloud.CloudCredentials; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.cobbzilla.util.collection.NameAndValue; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; + +@NoArgsConstructor @Accessors(chain=true) +public class CloudServiceConfig { + + @Getter @Setter private Map config = new LinkedHashMap<>(); + public boolean hasConfig() { return !empty(config); } + + public CloudServiceConfig addConfig(String name, String value) { + getConfig().put(name, value); + return this; + } + + @Getter @Setter private Map credentials = new LinkedHashMap<>(); + public boolean hasCredentials() { return !empty(credentials); } + + @JsonIgnore public CloudCredentials getCredentialsObject() { + return new CloudCredentials(NameAndValue.map2list(getCredentials()).toArray(NameAndValue.EMPTY_ARRAY)); + } + +} diff --git a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java index 7a3f908a..5b2611b7 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java +++ b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java @@ -13,9 +13,11 @@ import bubble.cloud.storage.StorageServiceDriver; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.account.Account; import bubble.model.account.AccountTemplate; +import bubble.model.boot.CloudServiceConfig; import bubble.server.BubbleConfiguration; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -23,22 +25,26 @@ import lombok.experimental.Accessors; import org.cobbzilla.util.collection.ArrayUtil; import org.cobbzilla.util.collection.ExpirationMap; import org.cobbzilla.util.collection.HasPriority; +import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.wizard.filters.Scrubbable; import org.cobbzilla.wizard.filters.ScrubbableField; import org.cobbzilla.wizard.model.Identifiable; import org.cobbzilla.wizard.model.entityconfig.IdentifiableBaseParentEntity; import org.cobbzilla.wizard.model.entityconfig.annotations.*; import org.cobbzilla.wizard.validation.HasValue; +import org.cobbzilla.wizard.validation.ValidationResult; import org.hibernate.annotations.Type; import javax.persistence.*; import javax.validation.constraints.Size; +import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static bubble.ApiConstants.EP_CLOUDS; import static bubble.cloud.storage.local.LocalStorageDriver.LOCAL_STORAGE; import static org.cobbzilla.util.daemon.ZillaRuntime.die; +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.*; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; @@ -75,6 +81,8 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun @HasValue(message="err.name.required") @ECIndex @Column(nullable=false, updatable=false, length=200) @Getter @Setter private String name; + @JsonIgnore public boolean isLocalStorage () { return name != null && name.equals(LOCAL_STORAGE); } + @JsonIgnore public boolean isNotLocalStorage () { return !isLocalStorage(); } @ECSearchable @ECField(index=20) @ECForeignKey(entity=Account.class) @@ -143,6 +151,10 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun @Transient public CloudCredentials getCredentials () { return credentialsJson == null ? null : json(credentialsJson, CloudCredentials.class); } public CloudService setCredentials (CloudCredentials credentials) { return setCredentialsJson(credentials == null ? null : json(credentials)); } + public boolean hasCredentials () { + final CloudCredentials creds = getCredentials(); + return creds != null && !empty(creds.getParams()); + } @Transient @JsonIgnore @Getter(lazy=true) private final CloudServiceDriver driver = initDriver(); private CloudServiceDriver initDriver () { @@ -152,17 +164,31 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun return d; } + @Transient @JsonIgnore public CloudServiceDriver getConfiguredDriver (BubbleConfiguration configuration) { + switch (getType()) { + case compute: return getComputeDriver(configuration); + case email: case sms: + case authenticator: return getAuthenticationDriver(configuration); + case geoLocation: return getGeoLocateDriver(configuration); + case geoCode: return getGeoCodeDriver(configuration); + case geoTime: return getGeoTimeDriver(configuration); + case dns: return getDnsDriver(configuration); + case storage: return getStorageDriver(configuration); + case payment: return getPaymentDriver(configuration); + default: + log.warn("getConfiguredDriver: unrecognized type: "+getType()); + case local: + return wireAndSetup(configuration); + } + } + @Transient @JsonIgnore public ComputeServiceDriver getComputeDriver (BubbleConfiguration configuration) { - final ComputeServiceDriver compute = wireAndSetup(configuration); - compute.startDriver(); - return compute; + return wireAndSetup(configuration); } @Transient @JsonIgnore public AuthenticationDriver getAuthenticationDriver(BubbleConfiguration configuration) { if (!getType().isAuthenticationType()) return die("getAuthenticationDriver: not an authentication type: "+getType()); - final AuthenticationDriver driver = wireAndSetup(configuration); - driver.startDriver(); - return driver; + return wireAndSetup(configuration); } @Transient @JsonIgnore public RegionalServiceDriver getRegionalDriver () { return (RegionalServiceDriver) getDriver(); } @@ -180,7 +206,7 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun return (DnsServiceDriver) wireAndSetup(configuration); } @Transient @JsonIgnore public StorageServiceDriver getStorageDriver(BubbleConfiguration configuration) { - if (!getName().equals(LOCAL_STORAGE)) { + if (isNotLocalStorage()) { final CloudCredentials credentials = getCredentials(); final BubbleNetwork thisNetwork = configuration.getThisNetwork(); if (thisNetwork != null && credentials.needsNewNetworkKey(thisNetwork.getUuid())) { @@ -217,4 +243,38 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun }); } + public CloudService configure(CloudServiceConfig config, ValidationResult errors) { + if (config.hasConfig()) { + final JsonNode driverConfig = getDriverConfig(); + for (Map.Entry cfg : config.getConfig().entrySet()) { + final String name = cfg.getKey(); + if (driverConfig == null || !driverConfig.has(name)) { + errors.addViolation("err.cloud.noSuchField", "driver config field does not exist: "+name, name); + } else if (errors.isValid()) { + ((ObjectNode) driverConfig).put(name, cfg.getValue()); + } + } + } + if (config.hasCredentials()) { + setCredentials(config.getCredentialsObject()); + } else { + setCredentials(null); + } + return this; + } + + public CloudServiceConfig toCloudConfig() { + final CloudServiceConfig config = new CloudServiceConfig(); + final JsonNode driverConfig = getDriverConfig(); + if (driverConfig != null) { + for (Iterator iter = driverConfig.fieldNames(); iter.hasNext(); ) { + final String field = iter.next(); + config.addConfig(field, driverConfig.get(field).textValue()); + } + } + if (hasCredentials()) { + config.setCredentials(NameAndValue.toMap(getCredentials().getParams())); + } + return config; + } } diff --git a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java index 3bd17389..258b70f3 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -8,6 +8,7 @@ import bubble.dao.cloud.BubbleNodeDAO; import bubble.model.CertType; import bubble.model.account.*; import bubble.model.account.message.*; +import bubble.model.boot.ActivationRequest; import bubble.model.cloud.BubbleNetwork; import bubble.model.cloud.BubbleNode; import bubble.model.cloud.NetworkKeys; @@ -78,15 +79,22 @@ public class AuthResource { @GET @Path(EP_ACTIVATE) public Response isActivated(@Context ContainerRequest ctx) { return ok(accountDAO.activated()); } + @GET @Path(EP_ACTIVATE+EP_CONFIGS) + public Response getActivationConfigs(@Context ContainerRequest ctx) { + final Account caller = optionalUserPrincipal(ctx); + if (accountDAO.activated() && (caller == null || !caller.admin())) return ok(); + return ok(activationService.getCloudDefaults()); + } + @Transactional @PUT @Path(EP_ACTIVATE) public Response activate(@Context Request req, @Context ContainerRequest ctx, @Valid ActivationRequest request) { if (request == null) return invalid("err.activation.request.required"); - final Account found = optionalUserPrincipal(ctx); - if (found != null) { - if (!found.admin()) return forbidden(); + final Account caller = optionalUserPrincipal(ctx); + if (caller != null) { + if (!caller.admin()) return forbidden(); return invalid("err.activation.alreadyDone", "activation has already been done"); } if (accountDAO.activated()) { diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java index 5b4d7fdb..ebc41bdf 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -13,7 +13,10 @@ import bubble.model.account.Account; import bubble.model.bill.AccountPaymentMethod; import bubble.model.bill.AccountPlan; import bubble.model.bill.BubblePlan; -import bubble.model.cloud.*; +import bubble.model.cloud.BubbleDomain; +import bubble.model.cloud.BubbleFootprint; +import bubble.model.cloud.BubbleNetwork; +import bubble.model.cloud.CloudService; import bubble.resources.account.AccountOwnedResource; import bubble.server.BubbleConfiguration; import lombok.extern.slf4j.Slf4j; @@ -31,7 +34,6 @@ import java.util.List; import java.util.stream.Collectors; import static bubble.ApiConstants.*; -import static bubble.cloud.storage.local.LocalStorageDriver.LOCAL_STORAGE; import static org.cobbzilla.wizard.resources.ResourceUtil.*; @Slf4j @@ -150,7 +152,7 @@ public class AccountPlansResource extends AccountOwnedResource storageClouds = cloudDAO.findByAccountAndType(caller.getUuid(), CloudServiceType.storage); // find the first one that is not LocalStorage - final List remoteStorage = storageClouds.stream().filter(c -> !c.getName().equals(LOCAL_STORAGE)).collect(Collectors.toList()); + final List remoteStorage = storageClouds.stream().filter(CloudService::isNotLocalStorage).collect(Collectors.toList()); if (!remoteStorage.isEmpty()) { // todo: storage should know what region it is in. diff --git a/bubble-server/src/main/java/bubble/server/BubbleServer.java b/bubble-server/src/main/java/bubble/server/BubbleServer.java index c7c5e9ad..26b575f7 100644 --- a/bubble-server/src/main/java/bubble/server/BubbleServer.java +++ b/bubble-server/src/main/java/bubble/server/BubbleServer.java @@ -13,8 +13,10 @@ import org.cobbzilla.wizard.server.RestServerBase; import org.cobbzilla.wizard.server.RestServerLifecycleListener; import org.cobbzilla.wizard.server.RestServerLifecycleListenerBase; import org.cobbzilla.wizard.server.config.factory.ConfigurationSource; +import org.cobbzilla.wizard.server.listener.BrowserLauncherListener; import org.cobbzilla.wizard.server.listener.FlywayMigrationListener; import org.cobbzilla.wizard.server.listener.SystemInitializerListener; +import org.slf4j.bridge.SLF4JBridgeHandler; import java.io.File; import java.util.Arrays; @@ -43,7 +45,8 @@ public class BubbleServer extends RestServerBase { new FlywayMigrationListener(), new NodeInitializerListener(), new DeviceInitializerListener(), - new BubbleFirstTimeListener() + new BubbleFirstTimeListener(), + new BrowserLauncherListener() }); public static final List RESTORE_LIFECYCLE_LISTENERS = Arrays.asList(new RestServerLifecycleListener[] { @@ -52,7 +55,8 @@ public class BubbleServer extends RestServerBase { public static final String[] DEFAULT_ENV_FILE_PATHS = { HOME_DIR + ".bubble.env", - HOME_DIR + "/current/bubble.env" + HOME_DIR + "/current/bubble.env", + System.getProperty("user.dir") + "/bubble.env" }; private static AtomicReference restoreKey = new AtomicReference<>(); @@ -67,6 +71,9 @@ public class BubbleServer extends RestServerBase { // config is loaded from the classpath public static void main(String[] args) throws Exception { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + final Map env = loadEnvironment(args); final ConfigurationSource configSource = getConfigurationSource(); diff --git a/bubble-server/src/main/java/bubble/service/boot/ActivationService.java b/bubble-server/src/main/java/bubble/service/boot/ActivationService.java index a1c34e91..ad02dea9 100644 --- a/bubble-server/src/main/java/bubble/service/boot/ActivationService.java +++ b/bubble-server/src/main/java/bubble/service/boot/ActivationService.java @@ -7,20 +7,29 @@ import bubble.cloud.compute.local.LocalComputeDriver; import bubble.cloud.dns.DnsServiceDriver; import bubble.cloud.storage.local.LocalStorageConfig; import bubble.cloud.storage.local.LocalStorageDriver; +import bubble.dao.account.AccountDAO; import bubble.dao.cloud.*; import bubble.model.account.Account; -import bubble.model.account.ActivationRequest; +import bubble.model.boot.ActivationRequest; +import bubble.model.boot.CloudServiceConfig; import bubble.model.cloud.*; import bubble.server.BubbleConfiguration; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.dns.DnsRecordMatch; import org.cobbzilla.util.dns.DnsType; +import org.cobbzilla.util.handlebars.HandlebarsUtil; +import org.cobbzilla.wizard.api.CrudOperation; +import org.cobbzilla.wizard.client.ApiClientBase; +import org.cobbzilla.wizard.model.Identifiable; +import org.cobbzilla.wizard.model.ModelSetupService; import org.cobbzilla.wizard.validation.SimpleViolationException; +import org.cobbzilla.wizard.validation.ValidationResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; -import java.util.Arrays; +import java.util.*; import static bubble.ApiConstants.ROOT_NETWORK_UUID; import static bubble.cloud.storage.StorageServiceDriver.STORAGE_PREFIX; @@ -30,14 +39,16 @@ import static bubble.model.cloud.BubbleFootprint.DEFAULT_FOOTPRINT_OBJECT; import static bubble.model.cloud.BubbleNetwork.TAG_ALLOW_REGISTRATION; import static bubble.model.cloud.BubbleNetwork.TAG_PARENT_ACCOUNT; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.io.FileUtil.toStringOrDie; import static org.cobbzilla.util.io.StreamUtil.stream2string; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.network.NetworkUtil.getFirstPublicIpv4; import static org.cobbzilla.util.network.NetworkUtil.getLocalhostIpv4; import static org.cobbzilla.util.system.CommandShell.execScript; +import static org.cobbzilla.util.system.Sleep.sleep; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Service @Slf4j @@ -45,7 +56,7 @@ public class ActivationService { public static final String DEFAULT_ROLES = "ansible/default_roles.json"; - public static final long ROOT_CREATE_TIMEOUT = SECONDS.toMillis(10); + public static final long ACTIVATION_TIMEOUT = SECONDS.toMillis(10); @Autowired private AnsibleRoleDAO roleDAO; @Autowired private CloudServiceDAO cloudDAO; @@ -56,6 +67,7 @@ public class ActivationService { @Autowired private BubbleNodeKeyDAO nodeKeyDAO; @Autowired private StandardSelfNodeService selfNodeService; @Autowired private BubbleConfiguration configuration; + @Autowired private ModelSetupService modelSetupService; public BubbleNode bootstrapThisNode(Account account, ActivationRequest request) { String ip = getFirstPublicIpv4(); @@ -65,35 +77,75 @@ public class ActivationService { } if (ip == null) die("bootstrapThisNode: no IP could be found, not even a localhost address"); - final CloudService publicDns = cloudDAO.create(new CloudService(request.getDns()) - .setType(CloudServiceType.dns) - .setTemplate(true) - .setAccount(account.getUuid())); - final DnsServiceDriver dnsDriver = publicDns.getDnsDriver(configuration); - final DnsRecordMatch nsMatcher = (DnsRecordMatch) new DnsRecordMatch() - .setType(DnsType.NS) - .setFqdn(request.getDomain().getName()); - checkDriver(dnsDriver, nsMatcher, "err.dns.testFailed", "err.dns.unknownError"); - - final CloudService localStorage = cloudDAO.create(new CloudService() - .setAccount(account.getUuid()) - .setType(CloudServiceType.storage) - .setDriverClass(LocalStorageDriver.class.getName()) - .setDriverConfigJson(json(new LocalStorageConfig().setBaseDir(configuration.getLocalStorageDir()))) - .setName(LOCAL_STORAGE) - .setTemplate(true)); + final Map requestConfigs = request.getCloudConfigs(); + final Map defaultConfigs = getCloudDefaultsMap(); + final ValidationResult errors = new ValidationResult(); + final List toCreate = new ArrayList<>(); + CloudService publicDns = null; + CloudService localStorage = null; + CloudService storage = null; + CloudService compute = null; + for (Map.Entry requestedCloud : requestConfigs.entrySet()) { + final String name = requestedCloud.getKey(); + final CloudServiceConfig config = requestedCloud.getValue(); + final CloudService defaultCloud = defaultConfigs.get(name); + if (defaultCloud == null) { + errors.addViolation("err.cloud.notFound", "No cloud template found with name: "+name, name); - final CloudService storage = request.getStorage(); - final CloudService networkStorage; - if (storage.getName().equals(LOCAL_STORAGE)) { - networkStorage = localStorage; - } else { - if (storage.getCredentials().needsNewNetworkKey(ROOT_NETWORK_UUID)) { - storage.setCredentials(storage.getCredentials().initNetworkKey(ROOT_NETWORK_UUID)); + } else if (errors.isValid()) { + final CloudService cloud = new CloudService(defaultCloud).configure(config, errors); + toCreate.add(cloud); + if (defaultCloud.getType() == CloudServiceType.dns && defaultCloud.getName().equals(request.getDomain().getPublicDns()) && publicDns == null) publicDns = cloud; + if (defaultCloud.getType() == CloudServiceType.storage && localStorage == null && defaultCloud.isLocalStorage()) localStorage = cloud; + if (defaultCloud.getType() == CloudServiceType.storage && storage == null) storage = cloud; + if (defaultCloud.getType() == CloudServiceType.compute && compute == null) compute = cloud; + } + } + if (publicDns == null) errors.addViolation("err.dns.noneSpecified"); + if (storage == null) errors.addViolation("err.storage.noneSpecified"); + if (compute == null && !configuration.testMode()) errors.addViolation("err.compute.noneSpecified"); + if (errors.isInvalid()) throw invalidEx(errors); + + // create local storage if it was not provided + if (localStorage == null) { + localStorage = cloudDAO.create(new CloudService() + .setAccount(account.getUuid()) + .setType(CloudServiceType.storage) + .setDriverClass(LocalStorageDriver.class.getName()) + .setDriverConfigJson(json(new LocalStorageConfig().setBaseDir(configuration.getLocalStorageDir()))) + .setName(LOCAL_STORAGE) + .setTemplate(true)); + } + + // create all clouds + CloudService networkStorage = localStorage; + for (CloudService cloud : toCreate) { + final CloudService c = cloudDAO.create(cloud + .setTemplate(true) + .setEnabled(true) + .setAccount(account.getUuid())); + if (cloud == publicDns) { + final DnsServiceDriver dnsDriver = publicDns.getDnsDriver(configuration); + if (cloud.getName().equals(request.getDomain().getPublicDns())) { + final DnsRecordMatch nsMatcher = (DnsRecordMatch) new DnsRecordMatch() + .setType(DnsType.NS) + .setFqdn(request.getDomain().getName()); + checkDriver(dnsDriver, nsMatcher, "err.dns.testFailed", "err.dns.unknownError"); + } else { + checkDriver(dnsDriver, null, "err.dns.testFailed", "err.dns.unknownError"); + } + + } else if (cloud == storage && cloud.isNotLocalStorage()) { + networkStorage = cloud; // prefer non-local storage for network + if (storage.getCredentials().needsNewNetworkKey(ROOT_NETWORK_UUID)) { + storage.setCredentials(storage.getCredentials().initNetworkKey(ROOT_NETWORK_UUID)); + } + final CloudServiceDriver storageDriver = networkStorage.getStorageDriver(configuration); + checkDriver(storageDriver, null, "err.storage.testFailed", "err.storage.unknownError"); + + } else { + checkDriver(cloud.getConfiguredDriver(configuration), null, "err."+cloud.getType()+".testFailed", "err."+cloud.getType()+".unknownError"); } - networkStorage = cloudDAO.create(new CloudService(storage).setAccount(account.getUuid())); - final CloudServiceDriver storageDriver = networkStorage.getStorageDriver(configuration); - checkDriver(storageDriver, null, "err.storage.testFailed", "err.storage.unknownError"); } final AnsibleRole[] roles = request.hasRoles() ? request.getRoles() : json(loadDefaultRoles(), AnsibleRole[].class); @@ -162,16 +214,33 @@ public class ActivationService { selfNodeService.initThisNode(node); configuration.refreshPublicSystemConfigs(); + if (request.createDefaultObjects()) { + background(() -> { + // wait for activation to complete + final AccountDAO accountDAO = configuration.getBean(AccountDAO.class); + final long start = now(); + while (!accountDAO.activated() && now() - start < ACTIVATION_TIMEOUT) { + sleep(SECONDS.toMillis(1), "waiting for activation to complete before creating default objects"); + } + if (!accountDAO.activated()) die("bootstrapThisNode: timeout waiting for activation to complete, default objects not created"); + + final ApiClientBase api = configuration.newApiClient().setToken(account.getToken()); + final Map> objects + = modelSetupService.setupModel(api, account, "manifest-dist"); + log.info("bootstrapThisNode: created default objects\n"+json(objects)); + }); + } + return node; } public void checkDriver(CloudServiceDriver driver, Object arg, String errTestFailed, String errException) { try { - if (!driver.test(arg)) throw invalidEx(errTestFailed); + if (!driver.test(arg)) throw invalidEx(errTestFailed, "test failed for driver: "+driver.getClass().getName()+(arg != null ? " with arg="+arg : ""), (arg == null ? null : arg.toString())); } catch (SimpleViolationException e) { throw e; } catch (Exception e) { - throw invalidEx(errException, shortError(e)); + throw invalidEx(errException, "test failed for driver: "+driver.getClass().getName()+(arg != null ? " with arg="+arg : "")+": "+shortError(e), (arg == null ? null : arg.toString())); } } @@ -191,4 +260,13 @@ public class ActivationService { return networkDAO.create(network); } + @Getter(lazy=true) private final CloudService[] cloudDefaults = initCloudDefaults(); + private CloudService[] initCloudDefaults() { + return json(HandlebarsUtil.apply(configuration.getHandlebars(), stream2string("models/dist/cloudService.json"), configuration.getEnvCtx()), CloudService[].class); + } + + @Getter(lazy=true) private final Map cloudDefaultsMap = initCloudDefaultsMap(); + private Map initCloudDefaultsMap() { + return Arrays.stream(getCloudDefaults()).collect(toMap(CloudService::getName, identity())); + } } diff --git a/bubble-server/src/main/resources/bubble-config.yml b/bubble-server/src/main/resources/bubble-config.yml index 9bb5c478..3b4482aa 100644 --- a/bubble-server/src/main/resources/bubble-config.yml +++ b/bubble-server/src/main/resources/bubble-config.yml @@ -3,7 +3,6 @@ serverName: bubble-api -#publicUriBase: https://127.0.0.1 publicUriBase: {{PUBLIC_BASE_URI}} defaultLocale: {{#exists BUBBLE_DEFAULT_LOCALE}}{{BUBBLE_DEFAULT_LOCALE}}{{else}}en_US{{/exists}} @@ -35,6 +34,7 @@ staticAssets: baseUri: / assetRoot: site/ localOverride: {{BUBBLE_ASSETS_DIR}} + singlePageApp: /index.html utilPaths: INDEX_PATH: /index.html INDEX_ALIASES: /:/index.php diff --git a/bubble-server/src/main/resources/logback.xml b/bubble-server/src/main/resources/logback.xml index 63b6c5d4..4bf9eb74 100644 --- a/bubble-server/src/main/resources/logback.xml +++ b/bubble-server/src/main/resources/logback.xml @@ -36,6 +36,7 @@ + diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties index 2d8fd07d..a646a797 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties @@ -75,7 +75,23 @@ form_section_title_credentials=Credentials form_section_title_config=Configuration button_label_activate=Activate -# Activation errors +# Activation cloud test errors +err.compute.testFailed=Error testing connection to Compute service +err.compute.unknownError=Error connecting to Compute service +err.geoCode.testFailed=Error testing connection to GeoCode service +err.geoCode.unknownError=Error connecting to GeoCode service +err.payment.testFailed=Error testing connection to Payment service +err.payment.unknownError=Error connecting to Payment service +err.geoLocation.testFailed=Error testing connection to GeoLocation service +err.geoLocation.unknownError=Error connecting to GeoLocation service +err.geoTime.testFailed=Error testing connection to GeoTime service +err.geoTime.unknownError=Error connecting to GeoTime service +err.email.testFailed=Error testing connection to Email service +err.email.unknownError=Error connecting to Email service +err.sms.testFailed=Error testing connection to SMS service +err.sms.unknownError=Error connecting to SMS service +err.authenticator.testFailed=Error testing connection to Authenticator service +err.authenticator.unknownError=Error connecting to Authenticator service err.dns.testFailed=Error testing connection to DNS service err.dns.unknownError=Error connecting to DNS service err.storage.testFailed=Error testing connection to Storage service @@ -113,6 +129,12 @@ err.token.invalid=Code is incorrect # Low-level errors err.driverConfig.initFailure=Cloud driver failed to initialize properlyu +err.cloud.noSuchField=A cloud driver config field name is invalid +err.cloud.notFound=No cloud exists with this name +err.dns.noneSpecified=No DNS service was configured +err.storage.noneSpecified=No Storage service was configured +err.compute.noneSpecified=No Compute service was configured +err.cloud.localStorageIsReservedName=LocalStorage is a reserved name # Entity config errors err.ec.param.invalid=Parameter is invalid diff --git a/bubble-server/src/main/resources/models/dist/cloudService.json b/bubble-server/src/main/resources/models/dist/cloudService.json index 4fe78842..4d62429f 100644 --- a/bubble-server/src/main/resources/models/dist/cloudService.json +++ b/bubble-server/src/main/resources/models/dist/cloudService.json @@ -1,15 +1,68 @@ [ { - "_subst": true, + "name": "Route53Dns", + "type": "dns", + "driverClass": "bubble.cloud.dns.route53.Route53DnsDriver", + "driverConfig": {}, + "credentials": { + "params": [ + {"name": "AWS_ACCESS_KEY_ID", "value": "{{AWS_ACCESS_KEY_ID}}"}, + {"name": "AWS_SECRET_KEY", "value": "{{AWS_SECRET_KEY}}"} + ] + }, + "template": true + }, + + { + "name": "GoDaddyDns", + "type": "dns", + "driverClass": "bubble.cloud.dns.godaddy.GoDaddyDnsDriver", + "driverConfig": {}, + "credentials": { + "params": [ + {"name": "GODADDY_API_KEY", "value": "{{GODADDY_API_KEY}}"}, + {"name": "GODADDY_API_SECRET", "value": "{{GODADDY_API_SECRET}}"} + ] + }, + "template": true + }, + + { + "name": "LocalStorage", + "type": "storage", + "driverClass": "bubble.cloud.storage.local.LocalStorageDriver", + "driverConfig": { "baseDir": "{{LOCALSTORAGE_BASE_DIR}}" }, + "template": false + }, + + { + "name": "S3_US_Standard", + "type": "storage", + "driverClass": "bubble.cloud.storage.s3.S3StorageDriver", + "driverConfig": { + "region": "US_EAST_1", + "bucket": "{{BUBBLE_S3_BUCKET}}", + "prefix": "{{BUBBLE_S3_BUCKET_PREFIX}}", + "listFetchSize": 100 + }, + "credentials": { + "params": [ + {"name": "AWS_ACCESS_KEY_ID", "value": "{{AWS_ACCESS_KEY_ID}}"}, + {"name": "AWS_SECRET_KEY", "value": "{{AWS_SECRET_KEY}}"} + ] + }, + "template": true + }, + + { "name": "GoogleGeoCoder", "type": "geoCode", "driverClass": "bubble.cloud.geoCode.google.GoogleGeoCodeDriver", - "credentials": { "params": [ {"name": "apiKey", "value": "{{required GOOGLE_API_KEY}}"} ] }, + "credentials": { "params": [ {"name": "apiKey", "value": "{{GOOGLE_API_KEY}}"} ] }, "template": true }, { - "_subst": true, "name": "TOTPAuthenticator", "type": "authenticator", "driverClass": "bubble.cloud.authenticator.TOTPAuthenticatorDriver", @@ -17,7 +70,6 @@ }, { - "_subst": true, "name": "SmtpServer", "type": "email", "driverClass": "{{BUBBLE_SMTP_DRIVER}}", @@ -26,42 +78,53 @@ }, "credentials": { "params": [ - {"name": "user", "value": "{{required BUBBLE_SMTP_USER}}"}, - {"name": "password", "value": "{{required BUBBLE_SMTP_PASS}}"}, - {"name": "host", "value": "{{required BUBBLE_SMTP_SERVER}}"}, - {"name": "port", "value": "{{required BUBBLE_SMTP_PORT}}"} + {"name": "user", "value": "{{BUBBLE_SMTP_USER}}"}, + {"name": "password", "value": "{{BUBBLE_SMTP_PASS}}"}, + {"name": "host", "value": "{{BUBBLE_SMTP_SERVER}}"}, + {"name": "port", "value": "{{BUBBLE_SMTP_PORT}}"} ] }, "template": true }, { - "_subst": true, "name": "TwilioSms", "type": "sms", "driverClass": "{{BUBBLE_SMS_DRIVER}}", "driverConfig": {}, "credentials": { "params": [ - {"name": "accountSID", "value": "{{required TWILIO_ACCOUNT_SID}}"}, - {"name": "authToken", "value": "{{required TWILIO_AUTH_TOKEN}}"}, - {"name": "fromPhoneNumber", "value": "{{required TWILIO_FROM_PHONE_NUMBER}}"} + {"name": "accountSID", "value": "{{TWILIO_ACCOUNT_SID}}"}, + {"name": "authToken", "value": "{{TWILIO_AUTH_TOKEN}}"}, + {"name": "fromPhoneNumber", "value": "{{TWILIO_FROM_PHONE_NUMBER}}"} ] }, "template": true }, { - "_subst": true, + "name": "MaxMind", + "type": "geoLocation", + "driverClass": "bubble.cloud.geoLocation.maxmind.MaxMindDriver", + "driverConfig": { + "url": "{{{MAXMIND_URL}}}", + "file": "{{{MAXMIND_FILE_REGEX}}}" + }, + "credentials": { + "params": [ {"name": "apiKey", "value": "{{{MAXMIND_API_KEY}}}"} ] + }, + "template": true + }, + + { "name": "GoogleGeoTime", "type": "geoTime", "driverClass": "bubble.cloud.geoTime.google.GoogleGeoTimeDriver", - "credentials": { "params": [ {"name": "apiKey", "value": "{{required GOOGLE_API_KEY}}"} ] }, + "credentials": { "params": [ {"name": "apiKey", "value": "{{GOOGLE_API_KEY}}"} ] }, "template": true }, { - "_subst": true, "name": "VultrCompute", "type": "compute", "driverClass": "bubble.cloud.compute.vultr.VultrDriver", @@ -140,14 +203,13 @@ }, "credentials": { "params": [ - {"name": "API-Key", "value": "{{required VULTR_API_KEY}}"} + {"name": "API-Key", "value": "{{VULTR_API_KEY}}"} ] }, "template": true }, { - "_subst": true, "name": "DigitalOceanCompute", "type": "compute", "driverClass": "bubble.cloud.compute.digitalocean.DigitalOceanDriver", @@ -198,7 +260,7 @@ }, "credentials": { "params": [ - {"name": "apiKey", "value": "{{required DIGITALOCEAN_API_KEY}}"} + {"name": "apiKey", "value": "{{DIGITALOCEAN_API_KEY}}"} ] }, "template": true diff --git a/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java b/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java index 7675df90..4af40834 100644 --- a/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java +++ b/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java @@ -5,7 +5,7 @@ import bubble.cloud.CloudServiceType; import bubble.cloud.dns.godaddy.GoDaddyDnsDriver; import bubble.cloud.storage.local.LocalStorageDriver; import bubble.model.account.Account; -import bubble.model.account.ActivationRequest; +import bubble.model.boot.ActivationRequest; import bubble.model.cloud.BubbleDomain; import bubble.model.cloud.CloudService; import bubble.server.BubbleConfiguration; @@ -109,8 +109,9 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { .setName(ROOT_USERNAME) .setPassword(ROOT_PASSWORD) .setNetworkName(hostname_short()) - .setDns(dns) - .setStorage(storage) + .addCloudConfig(dns) + .addCloudConfig(storage) + .setCreateDefaultObjects(false) .setDomain(domain), Account.class); } catch (ValidationException e) { if (e.hasViolations() && e.getViolations().containsKey("err.activation.alreadyDone")) { diff --git a/bubble-server/src/test/resources/models/system/cloudService.json b/bubble-server/src/test/resources/models/system/cloudService.json index 6ecf83bb..6aefee3c 100644 --- a/bubble-server/src/test/resources/models/system/cloudService.json +++ b/bubble-server/src/test/resources/models/system/cloudService.json @@ -6,8 +6,8 @@ "driverClass": "bubble.cloud.storage.s3.S3StorageDriver", "driverConfig": { "region": "US_EAST_1", - "bucket": "dev.bubblev.org", - "prefix": "2020-01", + "bucket": "{{BUBBLE_S3_BUCKET}}", + "prefix": "{{BUBBLE_S3_PREFIX}}", "listFetchSize": 100 }, "credentials": { diff --git a/bubble-server/src/test/resources/models/system/cloudService_live.json b/bubble-server/src/test/resources/models/system/cloudService_live.json index f7220f18..d33e866a 100644 --- a/bubble-server/src/test/resources/models/system/cloudService_live.json +++ b/bubble-server/src/test/resources/models/system/cloudService_live.json @@ -27,51 +27,6 @@ "params": [ {"name": "apiKey", "value": "{{{MAXMIND_API_KEY}}}"} ] }, "template": true - }, - - { - "_subst": true, - "name": "VultrCompute", - "type": "compute", - "driverClass": "bubble.cloud.compute.vultr.VultrDriver", - "driverConfig": { - "regions": [{ - "name": "Vultr - New Jersey", - "internalName": "New Jersey", - "location": {"city": "Newark", "country": "US", "region": "NJ", "lat": "40.72", "lon": "-74.17"} - }, { - "name": "Vultr - Atlanta", - "internalName": "Atlanta", - "location": {"city": "Atlanta", "country": "US", "region": "GA", "lat": "33.755", "lon": "-84.39"} - }, { - "name": "Vultr - Chicago", - "internalName": "Chicago", - "location": {"city": "Chicago", "country": "US", "region": "IL", "lat": "41.881944", "lon": "-87.627778"} - }, { - "name": "Vultr - San Jose", - "internalName": "Silicon Valley", - "location": {"city": "San Jose", "country": "US", "region": "CA", "lat": "37.333333", "lon": "-121.9"} - }, { - "name": "Vultr - London", - "internalName": "London", - "location": {"city": "London", "country": "GB", "region": "London", "lat": "51.507222", "lon": "-0.1275"} - }, { - "name": "Vultr - Paris", - "internalName": "Paris", - "location": {"city": "Paris", "country": "FR", "region": "Ile-de-Paris", "lat": "48.8567", "lon": "2.3508"} - }], - "sizes": [ - {"name": "small", "type": "small", "internalName": "1024 MB RAM,25 GB SSD,1.00 TB BW"}, - {"name": "medium", "type": "medium", "internalName": "4096 MB RAM,80 GB SSD,3.00 TB BW"}, - {"name": "large", "type": "large", "internalName": "8192 MB RAM,160 GB SSD,4.00 TB BW"} - ], - "config": [{"name": "os", "value": "Ubuntu 18.04 x64"}] - }, - "credentials": { - "params": [ - {"name": "API-Key", "value": "{{VULTR_API_KEY}}"} - ] - }, - "template": true } + ] \ No newline at end of file diff --git a/bubble-web b/bubble-web index 2b405967..89a819ba 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit 2b405967c0ea92f619ab43071e32f876cfb66b53 +Subproject commit 89a819baba806857daab90eb7539cbc9f1f665c1 diff --git a/config/activation.json b/config/activation.json new file mode 100644 index 00000000..90cceeb8 --- /dev/null +++ b/config/activation.json @@ -0,0 +1,102 @@ +{ + // name, password and description of the initial admin user + "name": "root", + "password": "REPLACE WITH YOUR ROOT PASSWORD", + "description": "root user", + + "cloudConfigs" : { + // You must configure at least one of these DNS services + // Comment the other one out if you're not going to use it + "Route53Dns" : { + "credentials" : { + "AWS_ACCESS_KEY_ID": "your_aws_access_key_id", + "AWS_SECRET_KEY": "your_aws_secret_key" + } + }, + "GoDaddyDns" : { + "credentials" : { + "GODADDY_API_KEY": "your_godaddy_api_key", + "GODADDY_API_SECRET": "your_godaddy_api_secret" + } + }, + + // You must configure at least one of these Compute services + // Comment the other one out if you're not going to use it + "VultrCompute": { + "credentials": {"API-Key": "your_vultr_api_key"} + }, + "DigitalOceanCompute": { + "credentials": {"apiKey": "your_digitalocean_api_key"} + }, + + // You must configure the AWS S3 Storage service in order to launch new Bubbles + "S3_US_Standard" : { + "config": { + // region must be a valid value from the Regions enum: https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/regions/Regions.java + "region": "US_EAST_1", + "bucket": "must be a valid bucket name that the credentials can read/write/list/delete", + "prefix": "", // optional: all paths for S3 operations will be prefixed with this path + "listFetchSize": 100 + }, + "credentials" : { + "AWS_ACCESS_KEY_ID": "your_aws_access_key_id", + "AWS_SECRET_KEY": "your_aws_secret_key" + } + }, + + // Required for TOTP-based authentication. Highly recommended. + "TOTPAuthenticator": {}, + + // OPTIONAL SERVICES + // Comment out any services you have not configured + + // Required for sending emails + "SmtpServer": { + "config": { + "tlsEnabled": true + }, + "credentials": { + "user": "your_smtp_username", + "password": "your_smtp_password", + "host": "your_smtp_server_hostname", + "port": "your_smtp_server_port" + } + }, + + // Required for sending SMS messages + "TwilioSms": { + "credentials": { + "accountSID": "your_twilio_account_SID", + "authToken": "your_twilio_auth_token", + // text messages sent by Bubble will come "from" this phone number, must be in Twilio-compatible format + "fromPhoneNumber": "your_twilio_from_number" + } + }, + + // Required for locale and "nearest compute region" auto-detection + "MaxMind": { + "config": { + // these values work for the free GeoLite database. you still have to specify an apiKey + "url": "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=[[apiKey]]&suffix=tar.gz", + "file": "GeoLite2-City_20[\\\\d]{6}/GeoLite2-City\\\\.mmdb" + }, + "credentials": {"apiKey": "your_maxmind_api_key"} + }, + + // Required for "nearest compute region" auto-detection + "GoogleGeoCoder": { + "credentials": {"apiKey": "your_google_api_key"} + }, + + // Required for timezone auto-detection + "GoogleGeoTime": { + "credentials": {"apiKey": "your_google_api_key"} + } + }, + + // the domain that new Bubbles will be launched within + "domain": { + "publicDns": "Route53Dns or GoDaddyDns", // name of a DNS provider configured above + "name": "example.com" // a domain name that you own, that is managed by the DNS provider named in `publicDns` + } +} diff --git a/dist-README.md b/dist-README.md index dd42cdcd..6fa21aea 100644 --- a/dist-README.md +++ b/dist-README.md @@ -18,8 +18,6 @@ will also be "yours only" -- all Bubbles disconnect from their launcher during c ## Getting Started -### Download a Bubble Distribution - ### Install PostgreSQL and Redis Install [PostgreSQL](https://www.postgresql.org/download/) if it is not installed on your system. It will probably be easier to install using an OS package, for example `sudo apt install postgresql` @@ -38,13 +36,26 @@ Otherwise, please either: * Create a PostgreSQL database named `bubble` and a database user named `bubble`. Set a password for the `bubble` user, and set the environment variable `BUBBLE_PG_PASSWORD` to this password when starting the Bubble launcher. +### Download a Bubble Distribution +Download and unzip the latest [Bubble Distribution ZIP](https://bubblev.com/download). + ### Start the Bubble launcher -Running a Bubble locally +Run the `./bin/run.sh` script to start the Bubble launcher. Once the server is running, it will try to open a browser window +to continue configuration. It will also print out the URL, so if the browser doesn't start correctly, you can paste this +into your browser's location bar. ### Activate your local Bubble +Your Bubble is running locally in a "blank" mode. It needs an initial "root" account and some basic services configured. + +#### Activate via Web UI +The browser-based admin UI should be displaying an "Activate" page. Complete the information on this page and submit the +data. The Bubble Launcher will create an initial "root" account and other basic system configurations. -#### Activate using the Web UI +#### Activate via command line +Copy the file in `config/activation.json`, then edit the file. There are comments in the file to guide you. +After saving the updated file, run this command: -#### Activate using the command line + `./bin/bactivate /path/to/activation.json` -### Configure Cloud Services +### Launch a new Bubble! +Using the web UI, click "Bubbles", select "New Bubble". Fill out and submit the New Bubble form, and your Bubble will be created! diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index 94b18c17..4f62fa3e 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit 94b18c1772002c013c1cc2a856126378aebc7460 +Subproject commit 4f62fa3e49dd90282d48860cf60a46e51a3f6aa0 diff --git a/utils/cobbzilla-wizard b/utils/cobbzilla-wizard index 76727a2b..9b6ec9b8 160000 --- a/utils/cobbzilla-wizard +++ b/utils/cobbzilla-wizard @@ -1 +1 @@ -Subproject commit 76727a2b02643af97d03535e474c483804664bce +Subproject commit 9b6ec9b885c1d42bee7a1e19c4e8e15c2f900941