diff --git a/configuration/installPluginsForEclipse2020-09.sh b/configuration/installPluginsForEclipse2020-09.sh new file mode 100644 index 00000000000..62ba0bb0bb1 --- /dev/null +++ b/configuration/installPluginsForEclipse2020-09.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# You need an installation of the 2020-09 release of "Eclipse IDE for Eclipse Committers" matching you OS and JDK (32 vs 64 Bit): +# https://www.eclipse.org/downloads/packages/release/2020-09/r/eclipse-ide-eclipse-committers + +if [[ $1 == "" ]]; then + echo "You need to specify the Eclipse installation directory" + exit 1 +fi + +installPath="$1" + +installPlugins() { + "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -installIU $2 + echo "" +} + +updatePlugins() { + "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -uninstallIU $2 -installIU $2 + echo "" +} + +uninstallPlugins() { + "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -uninstallIU $1 + echo "" +} + +# Not necessary if using Eclipse for JEE developers +echo "Installing webtools (WTP/WST/JPT) and m2e that are required by the GWT plugin..." +installPlugins http://download.eclipse.org/releases/2020-09/ org.eclipse.wst.web_ui.feature.feature.group,org.eclipse.jst.web_ui.feature.feature.group,org.eclipse.wst.xml_ui.feature.feature.group,org.eclipse.jpt.common.feature.feature.group,org.eclipse.jpt.jpa.feature.feature.group,org.eclipse.m2e.feature.feature.group,org.eclipse.m2e.wtp.feature.feature.group + +echo "Installing GWT plugin..." +installPlugins http://storage.googleapis.com/gwt-eclipse-plugin/v3/release com.gwtplugins.eclipse.suite.v3.feature.feature.group + +echo "Installing Mission Control plugin..." +installPlugins http://download.oracle.com/technology/products/missioncontrol/updatesites/base/6.0.0/eclipse com.oracle.jmc.feature.ide.feature.group,com.oracle.jmc.feature.flightrecorder.feature.group,com.oracle.jmc.feature.core.feature.group,com.oracle.jmc.feature.console.feature.group,com.oracle.jmc.feature.core.feature.group,com.oracle.jmc.feature.core.feature.group + +echo "Installing GWT SDM debug bridge..." +installPlugins http://p2.sapsailing.com/p2/sdbg com.github.sdbg.feature.feature.group + +echo "Installing EasyShell..." +installPlugins http://anb0s.github.io/EasyShell de.anbos.eclipse.easyshell.feature.feature.group + +echo "Installing Memory Analyzer..." +installPlugins http://download.eclipse.org/releases/2020-09 org.eclipse.mat.feature.feature.group,org.eclipse.mat.chart.feature.feature.group + +echo "Installing SAP JVM Tools (profiler) ..." +installPlugins https://tools.hana.ondemand.com/oxygen com.sap.jvm.profiling.feature.group + +echo "Installing UMLet ..." +installPlugins https://www.umlet.com/umlet_latest/repository/ umlet-eclipse-feature.feature.group + +echo "Installing javax.xml.bind for news feed polling (see https://stackoverflow.com/questions/52528693/eclipse-internal-error-polling-news-feeds)..." +installPlugins http://download.eclipse.org/tools/orbit/downloads/drops/R20200224183213/repository javax.xml.bind + +echo "Installing Enhanced Class Decompiler" +installPlugins https://ecd-plugin.github.io/update org.sf.feeling.decompiler.feature.group,org.sf.feeling.decompiler.cfr.feature.group,org.sf.feeling.decompiler.jad.feature.group,org.sf.feeling.decompiler.jd.feature.group,org.sf.feeling.decompiler.procyon.feature.group + +echo "Uninstalling Wild Web Developer" +uninstallPlugins org.eclipse.wildwebdeveloper.feature.feature.group + +echo "Installation completed!" diff --git a/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml b/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml index 9f7ae179fa2..1a7f58d5dde 100755 --- a/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml +++ b/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml @@ -2,21 +2,21 @@ diff --git a/java/com.amazon.aws.aws-java-api.updatesite/site.xml b/java/com.amazon.aws.aws-java-api.updatesite/site.xml index 7fd6f30502b..6ffe012824e 100755 --- a/java/com.amazon.aws.aws-java-api.updatesite/site.xml +++ b/java/com.amazon.aws.aws-java-api.updatesite/site.xml @@ -1,6 +1,6 @@ - + diff --git a/java/com.amazon.aws.aws-java-api/build.gradle b/java/com.amazon.aws.aws-java-api/build.gradle index 862c0802743..dd140de2709 100644 --- a/java/com.amazon.aws.aws-java-api/build.gradle +++ b/java/com.amazon.aws.aws-java-api/build.gradle @@ -5,13 +5,13 @@ repositories { } dependencies { - implementation platform('software.amazon.awssdk:bom:2.14.24') + implementation platform('software.amazon.awssdk:bom:2.15.16') implementation 'software.amazon.awssdk:s3' implementation 'software.amazon.awssdk:ec2' implementation 'software.amazon.awssdk:route53' implementation 'software.amazon.awssdk:cloudwatch' implementation 'software.amazon.awssdk:elasticloadbalancingv2' - implementation 'software.amazon.awssdk:firehose' + implementation 'software.amazon.awssdk:autoscaling' implementation group: 'software.amazon.awssdk', name: 'sdk-core', classifier: 'sources' implementation group: 'software.amazon.awssdk', name: 'arns', classifier: 'sources' implementation group: 'software.amazon.awssdk', name: 'aws-core', classifier: 'sources' @@ -24,7 +24,7 @@ dependencies { implementation group: 'software.amazon.awssdk', name: 'route53', classifier: 'sources' implementation group: 'software.amazon.awssdk', name: 'cloudwatch', classifier: 'sources' implementation group: 'software.amazon.awssdk', name: 'elasticloadbalancingv2', classifier: 'sources' - implementation group: 'software.amazon.awssdk', name: 'firehose', classifier: 'sources' + implementation group: 'software.amazon.awssdk', name: 'autoscaling', classifier: 'sources' } task downloadLibs(type: Copy) { into file('lib') diff --git a/java/com.amazon.aws.aws-java-api/pom.xml b/java/com.amazon.aws.aws-java-api/pom.xml index 3dded51a9ed..83c4b8e5f66 100644 --- a/java/com.amazon.aws.aws-java-api/pom.xml +++ b/java/com.amazon.aws.aws-java-api/pom.xml @@ -8,6 +8,6 @@ 1.0.0-SNAPSHOT com.amazon.aws.aws-java-api - 2.14.24 + 2.15.16 eclipse-plugin diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch index a5d93d85fd1..82541c2b777 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch @@ -20,7 +20,6 @@ - diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch index eedb7342807..0e40747c87a 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch @@ -22,7 +22,6 @@ - diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/MarkAlreadyUsedInRaceException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/MarkAlreadyUsedInRaceException.java index a804ab7853a..22e9cd1a20b 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/MarkAlreadyUsedInRaceException.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/MarkAlreadyUsedInRaceException.java @@ -3,14 +3,14 @@ package com.sap.sailing.domain.common.racelog.tracking; import java.io.Serializable; public class MarkAlreadyUsedInRaceException extends Exception implements Serializable { - - private static final long serialVersionUID = 2040461905745135256L; + + private static final long serialVersionUID = 2040461905745135256L; public MarkAlreadyUsedInRaceException() { super(); this.raceNames = ""; } - + public MarkAlreadyUsedInRaceException(String message, String raceNames) { super(message); this.raceNames = raceNames; diff --git a/java/com.sap.sailing.domain.igtimiadapter.gateway/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.igtimiadapter.gateway/META-INF/MANIFEST.MF index 91f62f7d8ca..890dcc954e1 100644 --- a/java/com.sap.sailing.domain.igtimiadapter.gateway/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.igtimiadapter.gateway/META-INF/MANIFEST.MF @@ -8,8 +8,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Require-Bundle: org.json.simple;bundle-version="1.1.0", com.sun.jersey;bundle-version="1.17.0", org.objectweb.asm;bundle-version="3.3.1", - org.apache.httpcomponents.httpclient;bundle-version="[4.2.5,4.3.0)", - org.apache.httpcomponents.httpcore;bundle-version="[4.0.0,4.3.0)", + org.apache.httpcomponents.httpclient;bundle-version="4.5.5", + org.apache.httpcomponents.httpcore;bundle-version="4.4.9", com.sap.sailing.domain.common, com.sap.sailing.domain.igtimiadapter.persistence, com.sap.sailing.domain.shared.android, diff --git a/java/com.sap.sailing.domain.igtimiadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.igtimiadapter/META-INF/MANIFEST.MF index 18426fa0674..fbd948f9ce4 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.igtimiadapter/META-INF/MANIFEST.MF @@ -8,8 +8,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Require-Bundle: org.json.simple;bundle-version="1.1.0", com.sun.jersey;bundle-version="1.17.0", org.objectweb.asm;bundle-version="3.3.1", - org.apache.httpcomponents.httpclient;bundle-version="[4.2.5,4.3.0)", - org.apache.httpcomponents.httpcore;bundle-version="[4.0.0,4.3.0)", + org.apache.httpcomponents.httpclient;bundle-version="4.5.5", + org.apache.httpcomponents.httpcore;bundle-version="4.4.9", com.sap.sailing.domain.common, com.sap.sailing.domain.igtimiadapter.persistence, com.sap.sailing.domain.shared.android, diff --git a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/ConnectivityUtils.java b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/ConnectivityUtils.java index 984b278e5f0..987236be597 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/ConnectivityUtils.java +++ b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/ConnectivityUtils.java @@ -16,7 +16,7 @@ import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -48,7 +48,7 @@ public class ConnectivityUtils { return result.toString(); } - public static HttpResponse postForm(String baseUrl, final String action, final Map inputFieldsToSubmit, DefaultHttpClient client, String referer) + public static HttpResponse postForm(String baseUrl, final String action, final Map inputFieldsToSubmit, CloseableHttpClient client, String referer) throws UnsupportedEncodingException, IOException, ClientProtocolException { HttpPost post = new HttpPost(baseUrl+action); List urlParameters = new ArrayList(); diff --git a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/IgtimiConnectionFactoryImpl.java b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/IgtimiConnectionFactoryImpl.java index 4dc43d64ccb..9823b5404d7 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/IgtimiConnectionFactoryImpl.java +++ b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/IgtimiConnectionFactoryImpl.java @@ -25,15 +25,13 @@ import org.apache.commons.lang.StringEscapeUtils; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; -import org.apache.http.client.RedirectStrategy; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.BasicCookieStore; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -115,7 +113,7 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory { } private Account getAccount(String creatorName, String accessToken) throws ClientProtocolException, IOException, IllegalStateException, ParseException { - HttpClient client = new SystemDefaultHttpClient(); + HttpClient client = HttpClientBuilder.create().build(); HttpGet getAccount = new HttpGet(getApiV1BaseUrl()+"account?access_token="+accessToken); HttpResponse accountResponse = client.execute(getAccount); JSONObject accountJson = ConnectivityUtils.getJsonFromResponse(accountResponse); @@ -375,7 +373,7 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory { * @throws RuntimeException in case there was an error while retrieving the token */ public Account obtainAccessTokenFromAuthorizationCode(String creatorName, String code) throws ClientProtocolException, IOException, IllegalStateException, ParseException { - HttpClient client = new SystemDefaultHttpClient(); + HttpClient client = HttpClientBuilder.create().build(); HttpPost post = new HttpPost(getOauthTokenUrl()); List urlParameters = new ArrayList(); urlParameters.add(new BasicNameValuePair("grant_type", "authorization_code")); @@ -402,42 +400,23 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory { * @return the authorization code which can then be used to obtain a permanent access token to be used by our client * to access data owned by the user identified by e-mail and password. */ - public String authorizeAndReturnAuthorizedCode(String userEmail, String userPassword) + public String authorizeAndReturnAuthorizedCode(final String userEmail, final String userPassword) throws ClientProtocolException, IOException, IllegalStateException, ParserConfigurationException, SAXException, ClassNotFoundException, InstantiationException, IllegalAccessException, ClassCastException { logger.info("Trying to authorize application client " + getClient().getId() + " for user " + userEmail); - DefaultHttpClient client = new SystemDefaultHttpClient(); - CookieStore cookieStore = new BasicCookieStore(); - client.setCookieStore(cookieStore); - client.setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()); + final RedirectStrategyExtractingAuthorizationCode codeExtractor = + new RedirectStrategyExtractingAuthorizationCode(new LaxRedirectStrategyForAllRedirectResponseCodes()); + CloseableHttpClient client = HttpClientBuilder.create() + .setDefaultCookieStore(new BasicCookieStore()) + .setRedirectStrategy(codeExtractor) + .build(); HttpGet get = new HttpGet(getOauthAuthorizeUrl()); HttpResponse responseForAuthorize = client.execute(get); - return signInAndReturnAuthorizationForm(client, responseForAuthorize, userEmail, userPassword); - } - - @Override - public Account createAccountToAccessUserData(String creatorName, String userEmail, String userPassword) throws ClientProtocolException, - IOException, IllegalStateException, ParserConfigurationException, SAXException, ClassNotFoundException, - InstantiationException, IllegalAccessException, ClassCastException, ParseException { - final Account result; - String code = authorizeAndReturnAuthorizedCode(userEmail, userPassword); - if (code != null) { - result = obtainAccessTokenFromAuthorizationCode(creatorName, code); - } else { - result = null; - } - return result; - } - - private String signInAndReturnAuthorizationForm(DefaultHttpClient client, HttpResponse response, - final String userEmail, final String userPassword) throws ParserConfigurationException, SAXException, - IOException, UnsupportedEncodingException, ClientProtocolException, IllegalStateException, - ClassNotFoundException, InstantiationException, IllegalAccessException, ClassCastException { SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); final String[] action = new String[1]; final Map inputFieldsToSubmit = new HashMap<>(); try { - String pageContent = ConnectivityUtils.getContent(response).replaceAll("", ""); + String pageContent = ConnectivityUtils.getContent(responseForAuthorize).replaceAll("", ""); parser.parse(new ByteArrayInputStream(pageContent.getBytes("UTF-8")), new DefaultHandler() { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) @@ -462,10 +441,7 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory { // swallow; we try to grab what we can; let's hope it was enough... logger.log(Level.FINE, "Exception trying to parse Igtimi authorization document", e); } - response.getEntity().getContent().close(); - final RedirectStrategy oldRedirectStrategy = client.getRedirectStrategy(); - final RedirectStrategyExtractingAuthorizationCode codeExtractor = new RedirectStrategyExtractingAuthorizationCode(oldRedirectStrategy); - client.setRedirectStrategy(codeExtractor); + responseForAuthorize.getEntity().getContent().close(); logger.info("Posting sign-in form for user "+userEmail); HttpResponse authorizationForm = ConnectivityUtils.postForm(getBaseUrl(), action[0], inputFieldsToSubmit, client, getSignInUrl()); if (codeExtractor.getCode() == null) { @@ -477,12 +453,26 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory { return codeExtractor.getCode(); } + @Override + public Account createAccountToAccessUserData(String creatorName, String userEmail, String userPassword) throws ClientProtocolException, + IOException, IllegalStateException, ParserConfigurationException, SAXException, ClassNotFoundException, + InstantiationException, IllegalAccessException, ClassCastException, ParseException { + final Account result; + String code = authorizeAndReturnAuthorizedCode(userEmail, userPassword); + if (code != null) { + result = obtainAccessTokenFromAuthorizationCode(creatorName, code); + } else { + result = null; + } + return result; + } + /** * Parses the form in the autorizationForm response and posts it by submitting the form that contains * the commit button with the value "Authorize". If a redirect strategy is set on the client it will * see the redirect URL in the Location header. The redirection target is closed immediately. */ - private void authorizeAndGetCode(HttpResponse authorizationForm, DefaultHttpClient client) + private void authorizeAndGetCode(HttpResponse authorizationForm, CloseableHttpClient client) throws IllegalStateException, SAXException, IOException, ParserConfigurationException, ClassNotFoundException, InstantiationException, IllegalAccessException, ClassCastException { // If the user already authorized the app, an empty document will be returned @@ -528,7 +518,7 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory { } public HttpClient getHttpClient() { - HttpClient client = new SystemDefaultHttpClient(); + HttpClient client = HttpClientBuilder.create().build(); return client; } diff --git a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/RedirectStrategyExtractingAuthorizationCode.java b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/RedirectStrategyExtractingAuthorizationCode.java index 3714d8e02bd..35a76c3b537 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/RedirectStrategyExtractingAuthorizationCode.java +++ b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/impl/RedirectStrategyExtractingAuthorizationCode.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.igtimiadapter.impl; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.Charset; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +67,7 @@ public class RedirectStrategyExtractingAuthorizationCode implements RedirectStra } private String getCodeFromRedirect(final Header locationHeader) throws URISyntaxException { - List params = URLEncodedUtils.parse(new URI(locationHeader.getValue()), "UTF-8"); + List params = URLEncodedUtils.parse(new URI(locationHeader.getValue()), Charset.forName("UTF-8")); for (NameValuePair param : params) { if (param.getName().equals("code")) { return param.getValue(); diff --git a/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF index 9cfff10364e..994354d93d1 100644 --- a/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF @@ -25,11 +25,11 @@ Export-Package: com.sap.sailing.domain.tractracadapter, com.sap.sailing.server.replication.test" Bundle-ActivationPolicy: lazy Import-Package: org.apache.http, - org.apache.http.client;version="[4.2.5,4.3.0)", - org.apache.http.client.entity;version="[4.2.5,4.3.0)", - org.apache.http.client.methods;version="[4.2.5,4.3.0)", - org.apache.http.impl.client;version="[4.2.5,4.3.0)", - org.apache.http.message;version="[4.2.5,4.3.0)", + org.apache.http.client;version="4.5.5", + org.apache.http.client.entity;version="4.5.5", + org.apache.http.client.methods;version="4.5.5", + org.apache.http.impl.client;version="4.5.5", + org.apache.http.message;version="4.4.9", org.json.simple, org.json.simple.parser, org.osgi.framework;version="1.7.0" diff --git a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF index 761e8bbf3d5..6befe174cb8 100755 --- a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF @@ -62,9 +62,9 @@ Import-Package: com.sap.sse.security, org.apache.commons.io.input;version="2.2.0", org.apache.commons.lang;version="2.6.0", org.apache.http, - org.apache.http.client;version="[4.2.5,4.3.0)", - org.apache.http.client.entity;version="[4.2.5,4.3.0)", - org.apache.http.client.methods;version="[4.2.5,4.3.0)", - org.apache.http.impl.client;version="[4.2.5,4.3.0)", - org.apache.http.message;version="[4.2.5,4.3.0)" + org.apache.http.client;version="4.5.5", + org.apache.http.client.entity;version="4.5.5", + org.apache.http.client.methods;version="4.5.5", + org.apache.http.impl.client;version="4.5.5", + org.apache.http.message;version="4.4.9" Automatic-Module-Name: com.sap.sailing.domain diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/orc/impl/ORCPublicCertificateDatabaseImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/orc/impl/ORCPublicCertificateDatabaseImpl.java index 4260a5d1288..ce4d129d23f 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/orc/impl/ORCPublicCertificateDatabaseImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/orc/impl/ORCPublicCertificateDatabaseImpl.java @@ -40,7 +40,7 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.w3c.dom.DOMException; import org.w3c.dom.Document; @@ -277,7 +277,7 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat public Iterable search(CountryCode issuingCountry, Integer yearOfIssuance, String referenceNumber, String yachtName, String sailNumber, String boatClassName, boolean includeInvalid) throws Exception { final Set result = new HashSet<>(); - final HttpClient client = new SystemDefaultHttpClient(); + final HttpClient client = HttpClientBuilder.create().build(); final List params = new ArrayList<>(); params.add(ACTION_PARAM); params.add(XSLP_PARAM); @@ -496,7 +496,7 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat private ORCCertificate getSingleCertificate(final String queryParameters) throws IOException, org.json.simple.parser.ParseException, ClientProtocolException { final HttpGet getRequest = new HttpGet(SINGLE_CERTIFICATE_DOWNLOAD_URL+"&"+queryParameters); - final HttpClient client = new SystemDefaultHttpClient(); + final HttpClient client = HttpClientBuilder.create().build(); addAuthorizationHeader(getRequest); final Iterable certificates = new ORCCertificatesJsonImporter().read(client.execute(getRequest).getEntity().getContent()) .getCertificates(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/FinishTimeUpdateHandler.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/FinishTimeUpdateHandler.java index e5c027ec562..62a4f388d03 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/FinishTimeUpdateHandler.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/FinishTimeUpdateHandler.java @@ -13,8 +13,8 @@ import java.util.logging.Logger; import org.apache.http.HttpResponse; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.json.simple.parser.ParseException; @@ -59,8 +59,9 @@ public class FinishTimeUpdateHandler extends UpdateHandler { params.add(new BasicNameValuePair(FIELD_TRACKING_END_TIME, String.valueOf(newFinishedTime.plus( TrackedRace.STOP_TRACKING_THIS_MUCH_AFTER_RACE_FINISH).asMillis()))); request.setEntity(new UrlEncodedFormEntity(params)); - final AbstractHttpClient client = new SystemDefaultHttpClient(); - client.setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()); + final CloseableHttpClient client = HttpClientBuilder.create() + .setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()) + .build(); logger.info("Using " + stopTrackingURI.toString() + " to stop tracking"); final HttpResponse response = client.execute(request); try { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/StartTimeUpdateHandler.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/StartTimeUpdateHandler.java index b33650f08d4..32748350efd 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/StartTimeUpdateHandler.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/StartTimeUpdateHandler.java @@ -15,10 +15,10 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.json.simple.parser.ParseException; @@ -92,8 +92,9 @@ public class StartTimeUpdateHandler extends UpdateHandler implements StartTimeCh params.add(new BasicNameValuePair(FIELD_TRACKING_START_TIME, String.valueOf(newStartTime.minus( TrackedRace.START_TRACKING_THIS_MUCH_BEFORE_RACE_START).asMillis()))); request.setEntity(new UrlEncodedFormEntity(params)); - final AbstractHttpClient client = new SystemDefaultHttpClient(); - client.setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()); + final HttpClient client = HttpClientBuilder.create() + .setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()) + .build(); logger.info("Using " + startTrackingURI.toString() + " to start tracking"); final HttpResponse response = client.execute(request); try { diff --git a/java/com.sap.sailing.feature.runtime/feature.xml b/java/com.sap.sailing.feature.runtime/feature.xml index 6c196d28d02..c300bbc664a 100644 --- a/java/com.sap.sailing.feature.runtime/feature.xml +++ b/java/com.sap.sailing.feature.runtime/feature.xml @@ -32,7 +32,7 @@ id="org.apache.commons.codec" download-size="0" install-size="0" - version="1.6.0.v201305230611" + version="1.10.0.v20180409-1845" unpack="false"/> - diff --git a/java/com.sap.sailing.gwt.ui/GWT Sailing SDM AdminConsole.launch b/java/com.sap.sailing.gwt.ui/GWT Sailing SDM AdminConsole.launch new file mode 100644 index 00000000000..bc30cd52808 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/GWT Sailing SDM AdminConsole.launch @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index 158ce5f3afe..567e7ca4e86 100644 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -71,7 +71,7 @@ Import-Package: javax.servlet;version="3.1.0", javax.servlet.http;version="3.1.0", javax.ws.rs;version="1.1.1", javax.ws.rs.core;version="1.1.1", - org.apache.http.client;version="[4.2.5,4.3.0)", + org.apache.http.client;version="4.5.5", org.json.simple, org.json.simple.parser, org.osgi.framework, diff --git a/java/com.sap.sailing.gwt.ui/js/videojs-panorama.v5.min.js b/java/com.sap.sailing.gwt.ui/js/videojs-panorama.v5.min.js index bc96c76f68a..09380af33cc 100644 --- a/java/com.sap.sailing.gwt.ui/js/videojs-panorama.v5.min.js +++ b/java/com.sap.sailing.gwt.ui/js/videojs-panorama.v5.min.js @@ -1 +1 @@ -!function e(t,i,n){function a(s,o){if(!i[s]){if(!t[s]){var h="function"==typeof require&&require;if(!o&&h)return h(s,!0);if(r)return r(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var c=i[s]={exports:{}};t[s][0].call(c.exports,function(e){var i=t[s][1][e];return a(i?i:e)},c,c.exports,e,t,i,n)}return i[s].exports}for(var r="function"==typeof require&&require,s=0;s=e.video.duration}function d(e){var t=this;t.video.readyState>=t.video.HAVE_FUTURE_DATA?(t.hasAudio||(t.driver.currentTime=t.video.currentTime+e*t.video.playbackRate/1e3,t.video.loop&&c(t)&&(t.driver.currentTime=0)),l(t.video,t.driver.currentTime)):t.video.networkState!==t.video.NETWORK_IDLE||t.video.buffered.length||t.video.load(),t.video.ended&&(delete t.video[T],t.video.pause(!0))}function u(){var e=this,t=e[y];return e.webkitDisplayingFullscreen?void e[x]():("data:"!==t.driver.src&&t.driver.src!==e.src&&(l(e,0,!0),t.driver.src=e.src),void(e.paused&&(t.paused=!1,e.buffered.length||e.load(),t.driver.play(),t.updater.start(),t.hasAudio||(o(e,"play"),t.video.readyState>=t.video.HAVE_ENOUGH_DATA&&o(e,"playing")))))}function f(e){var t=this,i=t[y];i.driver.pause(),i.updater.stop(),t.webkitDisplayingFullscreen&&t[L](),i.paused&&!e||(i.paused=!0,i.hasAudio||o(t,"pause"),t.ended&&(t[T]=!0,o(t,"ended")))}function v(e,t){var i=e[y]={};i.paused=!0,i.hasAudio=t,i.video=e,i.updater=w.frameIntervalometer(d.bind(i)),t?i.driver=h(e):(e.addEventListener("canplay",function(){e.paused||o(e,"playing")}),i.driver={src:e.src||e.currentSrc||"data:",muted:!0,paused:!0,pause:function(){i.driver.paused=!0},play:function(){i.driver.paused=!1,c(i)&&l(e,0)},get ended(){return c(i)}}),e.addEventListener("emptied",function(){var t=!i.driver.src||"data:"===i.driver.src;i.driver.src&&i.driver.src!==e.src&&(l(e,0,!0),i.driver.src=e.src,t?i.driver.play():i.updater.stop())},!1),e.addEventListener("webkitbeginfullscreen",function(){e.paused?t&&!i.driver.buffered.length&&i.driver.load():(e.pause(),e[x]())}),t&&(e.addEventListener("webkitendfullscreen",function(){i.driver.currentTime=e.currentTime}),e.addEventListener("seeking",function(){R.indexOf(100*e.currentTime|0)<0&&(i.driver.currentTime=e.currentTime)}))}function p(e){var t=e[y];e[x]=e.play,e[L]=e.pause,e.play=u,e.pause=f,r(e,"paused",t.driver),r(e,"muted",t.driver,!0),r(e,"playbackRate",t.driver,!0),r(e,"ended",t.driver),r(e,"loop",t.driver,!0),a(e,"seeking"),a(e,"seeked"),a(e,"timeupdate",T,!1),a(e,"ended",T,!1)}function m(e,t,i){void 0===t&&(t=!0),void 0===i&&(i=!0),i&&!M||e[y]||(v(e,t),p(e),e.classList.add("IIV"),!t&&e.autoplay&&e.play(),/iPhone|iPod|iPad/.test(navigator.platform)||console.warn("iphone-inline-video is not guaranteed to work in emulated environments"))}var g,b=n(e("poor-mans-symbol")),w=e("intervalometer"),M=/iPhone|iPod/i.test(navigator.userAgent)&&!matchMedia("(-webkit-video-playable-inline)").matches,y=b(),T=b(),x=b("nativeplay"),L=b("nativepause"),R=[],C=0;m.isWhitelisted=M,t.exports=m},{intervalometer:1,"poor-mans-symbol":3}],3:[function(e,t,i){"use strict";var n="undefined"==typeof Symbol?function(e){return"@"+(e||"@")+Math.random()}:Symbol;t.exports=n},{}],4:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(i,"__esModule",{value:!0});var a=e("../lib/Detector"),r=n(a),s=e("../lib/MobileBuffering"),o=n(s),h=e("../lib/Util"),l=n(h),c=2,d=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return{constructor:function(n,a){this.settings=a,this.width=n.el().offsetWidth,this.height=n.el().offsetHeight,this.lon=a.initLon,this.lat=a.initLat,this.phi=0,this.theta=0,this.videoType=a.videoType,this.clickToToggle=a.clickToToggle,this.mouseDown=!1,this.isUserInteracting=!1,this.renderer=new t.WebGLRenderer,this.renderer.setPixelRatio(window.devicePixelRatio),this.renderer.setSize(this.width,this.height),this.renderer.autoClear=!1,this.renderer.setClearColor(0,1);var s=i.getTech(n);if(this.supportVideoTexture=r.default.supportVideoTexture(),this.liveStreamOnSafari=r.default.isLiveStreamOnSafari(s),this.liveStreamOnSafari&&(this.supportVideoTexture=!1),this.supportVideoTexture)this.texture=new t.Texture(s);else{this.helperCanvas=n.addChild("HelperCanvas",{video:s,width:a.helperCanvas.width?a.helperCanvas.width:this.width,height:a.helperCanvas.height?a.helperCanvas.height:this.height});var o=this.helperCanvas.el();this.texture=new t.Texture(o)}s.style.display="none",this.texture.generateMipmaps=!1,this.texture.minFilter=t.LinearFilter,this.texture.maxFilter=t.LinearFilter,this.texture.format=t.RGBFormat,this.el_=this.renderer.domElement,this.el_.classList.add("vjs-video-canvas"),a.el=this.el_,e.call(this,n,a),this.attachControlEvents(),this.player().on("play",function(){this.time=(new Date).getTime(),this.animate()}.bind(this))},attachControlEvents:function(){this.on("mousemove",this.handleMouseMove.bind(this)),this.on("touchmove",this.handleTouchMove.bind(this)),this.on("mousedown",this.handleMouseDown.bind(this)),this.on("touchstart",this.handleTouchStart.bind(this)),this.on("mouseup",this.handleMouseUp.bind(this)),this.on("touchend",this.handleTouchEnd.bind(this)),this.settings.scrollable&&(this.on("mousewheel",this.handleMouseWheel.bind(this)),this.on("MozMousePixelScroll",this.handleMouseWheel.bind(this))),this.on("mouseenter",this.handleMouseEnter.bind(this)),this.on("mouseleave",this.handleMouseLease.bind(this))},handleResize:function(){this.width=this.player().el().offsetWidth,this.height=this.player().el().offsetHeight,this.renderer.setSize(this.width,this.height)},handleMouseUp:function(e){if(this.mouseDown=!1,this.clickToToggle){var t=e.clientX||e.changedTouches&&e.changedTouches[0].clientX,i=e.clientY||e.changedTouches&&e.changedTouches[0].clientY;if("undefined"==typeof t||"undefined"===i)return;var n=Math.abs(t-this.onPointerDownPointerX),a=Math.abs(i-this.onPointerDownPointerY);n<.1&&a<.1&&(this.player().paused()?this.player().play():this.player().pause())}},handleMouseDown:function(e){e.preventDefault();var t=e.clientX||e.touches&&e.touches[0].clientX,i=e.clientY||e.touches&&e.touches[0].clientY;"undefined"!=typeof t&&"undefined"!==i&&(this.mouseDown=!0,this.onPointerDownPointerX=t,this.onPointerDownPointerY=i,this.onPointerDownLon=this.lon,this.onPointerDownLat=this.lat)},handleTouchStart:function(e){e.touches.length>1&&(this.isUserPinch=!0,this.multiTouchDistance=l.default.getTouchesDistance(e.touches)),this.handleMouseDown(e)},handleTouchEnd:function(e){this.isUserPinch=!1,this.handleMouseUp(e)},handleMouseMove:function(e){var t=e.clientX||e.touches&&e.touches[0].clientX,i=e.clientY||e.touches&&e.touches[0].clientY;if("undefined"!=typeof t&&"undefined"!==i)if(this.settings.clickAndDrag)this.mouseDown&&(this.lon=.2*(this.onPointerDownPointerX-t)+this.onPointerDownLon,this.lat=.2*(i-this.onPointerDownPointerY)+this.onPointerDownLat);else{var n=e.pageX-this.el_.offsetLeft,a=e.pageY-this.el_.offsetTop;this.lon=n/this.width*430-225,this.lat=a/this.height*-180+90}},handleTouchMove:function(e){(!this.isUserPinch||e.touches.length<=1)&&this.handleMouseMove(e)},handleMobileOrientation:function(e){if("undefined"!=typeof e.rotationRate){var t=e.rotationRate.alpha,i=e.rotationRate.beta,n="undefined"!=typeof e.portrait?e.portrait:window.matchMedia("(orientation: portrait)").matches,a="undefined"!=typeof e.landscape?e.landscape:window.matchMedia("(orientation: landscape)").matches,r=e.orientation||window.orientation;if(n)this.lon=this.lon-i*this.settings.mobileVibrationValue,this.lat=this.lat+t*this.settings.mobileVibrationValue;else if(a){var s=-90;"undefined"!=typeof r&&(s=r),this.lon=s==-90?this.lon+t*this.settings.mobileVibrationValue:this.lon-t*this.settings.mobileVibrationValue,this.lat=s==-90?this.lat+i*this.settings.mobileVibrationValue:this.lat-i*this.settings.mobileVibrationValue}}},handleMouseWheel:function(e){e.stopPropagation(),e.preventDefault()},handleMouseEnter:function(e){this.isUserInteracting=!0},handleMouseLease:function(e){this.isUserInteracting=!1,this.mouseDown&&(this.mouseDown=!1)},animate:function(){if(this.requestAnimationId=requestAnimationFrame(this.animate.bind(this)),!this.player().paused()&&"undefined"!=typeof this.texture&&(!this.isPlayOnMobile&&this.player().readyState()>=c||this.isPlayOnMobile&&this.player().hasClass("vjs-playing"))){var e=(new Date).getTime();if(e-this.time>=30&&(this.texture.needsUpdate=!0,this.time=e),this.isPlayOnMobile){var t=this.player().currentTime();o.default.isBuffering(t)?this.player().hasClass("vjs-panorama-mobile-inline-video-buffering")||this.player().addClass("vjs-panorama-mobile-inline-video-buffering"):this.player().hasClass("vjs-panorama-mobile-inline-video-buffering")&&this.player().removeClass("vjs-panorama-mobile-inline-video-buffering")}}this.render()},render:function(){if(!this.isUserInteracting){var e=this.lat>this.settings.initLat?-1:1,i=this.lon>this.settings.initLon?-1:1;this.settings.backToVerticalCenter&&(this.lat=this.lat>this.settings.initLat-Math.abs(this.settings.returnStepLat)&&this.latthis.settings.initLon-Math.abs(this.settings.returnStepLon)&&this.lon2&&void 0!==arguments[2]?arguments[2]:{},n=(0,r.default)(e,t,i);return o.default.extend(n,{constructor:function(e,i){n.constructor.call(this,e,i),this.VRMode=!1,this.scene=new t.Scene,this.camera=new t.PerspectiveCamera(i.initFov,this.width/this.height,1,2e3),this.camera.target=new t.Vector3(0,0,0);var a="equirectangular"===this.videoType?new t.SphereGeometry(500,60,40):new t.SphereBufferGeometry(500,60,40).toNonIndexed();if("fisheye"===this.videoType){for(var r=a.attributes.normal.array,s=a.attributes.uv.array,o=0,h=r.length/3;o=13},isLiveStreamOnSafari:function(e){for(var t=e.querySelectorAll("source"),i=!1,n=0;nWebGL.
','Find out how to get it here.'].join("\n"):['Your browser does not seem to support WebGL.
','Find out how to get it here.'].join("\n")),e},addGetWebGLMessage:function(e){var t,i,a;e=e||{},t=void 0!==e.parent?e.parent:document.body,i=void 0!==e.id?e.id:"oldie",a=n.getWebGLErrorMessage(),a.id=i,t.appendChild(a)}};i.default=n},{}],7:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n=document.createElement("canvas");n.className="vjs-video-helper-canvas";var a=function(e){return{constructor:function(t,i){this.videoElement=i.video,this.width=i.width,this.height=i.height,n.width=this.width,n.height=this.height,n.style.display="none",i.el=n,this.context=n.getContext("2d"),this.context.drawImage(this.videoElement,0,0,this.width,this.height),e.call(this,t,i)},getContext:function(){return this.context},update:function(){this.context.drawImage(this.videoElement,0,0,this.width,this.height)},el:function(){return n}}};i.default=a},{}],8:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n={prev_currentTime:0,counter:0,isBuffering:function(e){return e==this.prev_currentTime?this.counter++:this.counter=0,this.prev_currentTime=e,this.counter>10&&(this.counter=10,!0)}};i.default=n},{}],9:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a=function(e){var t=document.createElement("div");return t.className="vjs-video-notice-label",{constructor:function(i,a){"object"==n(a.NoticeMessage)?(t=a.NoticeMessage,a.el=a.NoticeMessage):"string"==typeof a.NoticeMessage&&(t.innerHTML=a.NoticeMessage,a.el=t),e.call(this,i,a)},el:function(){return t}}};i.default=a},{}],10:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(i,"__esModule",{value:!0});var a=e("./BaseCanvas"),r=n(a),s=e("./Util"),o=n(s),h=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=(0,r.default)(e,t,i);return o.default.extend(n,{constructor:function(e,i){n.constructor.call(this,e,i),this.VRMode=!1,this.scene=new t.Scene;var a=this.width/this.height;this.cameraL=new t.PerspectiveCamera(i.initFov,a,1,2e3),this.cameraL.target=new t.Vector3(0,0,0),this.cameraR=new t.PerspectiveCamera(i.initFov,a/2,1,2e3),this.cameraR.position.set(1e3,0,0),this.cameraR.target=new t.Vector3(1e3,0,0);for(var r=new t.SphereBufferGeometry(500,60,40).toNonIndexed(),s=new t.SphereBufferGeometry(500,60,40).toNonIndexed(),o=r.attributes.uv.array,h=r.attributes.normal.array,l=0;l1&&void 0!==arguments[1]?arguments[1]:{};for(var i in e)e.hasOwnProperty(i)&&!t.hasOwnProperty(i)&&(t[i]=e[i]);return t}function d(e){var t={};for(var i in e)t[i]=e[i];return t}function u(e){return Math.sqrt((e[0].clientX-e[1].clientX)*(e[0].clientX-e[1].clientX)+(e[0].clientY-e[1].clientY)*(e[0].clientY-e[1].clientY))}Object.defineProperty(i,"__esModule",{value:!0}),i.default={whichTransitionEvent:n,mobileAndTabletcheck:a,isIos:r,isRealIphone:s,fovToProjection:l,extend:c,deepCopy:d,getTouchesDistance:u}},{}],12:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n=function(e){return{constructor:function(t,i){e.call(this,t,i)},buildCSSClass:function(){return"vjs-VR-control "+e.prototype.buildCSSClass.call(this)},handleClick:function(){var e=this.player().getChild("Canvas");e.VRMode?e.disableVR():e.enableVR(),e.VRMode?this.addClass("enable"):this.removeClass("enable"),e.VRMode?this.player().trigger("VRModeOn"):this.player().trigger("VRModeOff")},controlText_:"VR"}};i.default=n},{}],13:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=e.getChild("Canvas");return function(){e.el().style.width=window.innerWidth+"px",e.el().style.height=window.innerHeight+"px",t.handleResize()}}function r(e,t){var i=a(e);e.controlBar.fullscreenToggle.off("tap",t),e.controlBar.fullscreenToggle.on("tap",function(){var t=e.getChild("Canvas");e.isFullscreen()?(e.isFullscreen(!1),e.exitFullWindow(),e.el().style.width="",e.el().style.height="",t.handleResize(),window.removeEventListener("devicemotion",i)):(e.isFullscreen(!0),e.enterFullWindow(),i(),window.addEventListener("devicemotion",i))})}Object.defineProperty(i,"__esModule",{value:!0});var s=e("./lib/Util"),o=n(s),h=e("./lib/Detector"),l=n(h),c=e("iphone-inline-video"),d=n(c),u=o.default.mobileAndTabletcheck(),f={clickAndDrag:u,showNotice:!0,NoticeMessage:"Please use your mouse drag and drop the video.",autoHideNotice:3e3,scrollable:!0,initFov:75,maxFov:105,minFov:51,initLat:0,initLon:-180,returnStepLat:.5,returnStepLon:2,backToVerticalCenter:!u,backToHorizonCenter:!u,clickToToggle:!1,minLat:-85,maxLat:85,minLon:-(1/0),maxLon:1/0,videoType:"equirectangular",rotateX:0,rotateY:0,rotateZ:0,autoMobileOrientation:!1,mobileVibrationValue:o.default.isIos()?.022:1,VREnable:!0,VRGapDegree:2.5,closePanorama:!1,helperCanvas:{},dualFish:{width:1920,height:1080,circle1:{x:.240625,y:.553704,rx:.23333,ry:.43148,coverX:.913,coverY:.9},circle2:{x:.757292,y:.553704,rx:.232292,ry:.4296296,coverX:.913,coverY:.9308}}},v=function(e,t,i){if(e.addClass("vjs-panorama"),!l.default.webgl)return p(e,{NoticeMessage:l.default.getWebGLErrorMessage(),autoHideNotice:t.autoHideNotice}),void(t.callback&&t.callback());e.addChild("Canvas",o.default.deepCopy(t));var n=e.getChild("Canvas");if(u){var a=i.getTech(e);o.default.isRealIphone()&&(a.setAttribute("playsinline",""),(0,d.default)(a,!0)),o.default.isIos()&&r(e,i.getFullscreenToggleClickFn(e)),e.addClass("vjs-panorama-mobile-inline-video"),e.removeClass("vjs-using-native-controls"),n.playOnMobile()}t.showNotice&&e.on("playing",function(){p(e,o.default.deepCopy(t))}),t.VREnable&&e.controlBar.addChild("VRButton",{},e.controlBar.children().length-1),n.hide(),e.on("play",function(){n.show()}),e.on("fullscreenchange",function(){n.handleResize()}),t.callback&&t.callback()},p=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{NoticeMessage:""},i=e.addChild("Notice",t);t.autoHideNotice>0&&setTimeout(function(){i.addClass("vjs-video-notice-fadeOut");var e=o.default.whichTransitionEvent(),t=function t(){i.hide(),i.removeClass("vjs-video-notice-fadeOut"),i.off(e,t)};i.on(e,t)},t.autoHideNotice)},m=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=["equirectangular","fisheye","3dVideo","dual_fisheye"],i=function(i){var n=this;return e.mergeOption&&(i=e.mergeOption(f,i)),"undefined"==typeof e._init||"function"!=typeof e._init?void console.error("plugin must implement init function()."):(t.indexOf(i.videoType)==-1&&(i.videoType=f.videoType),e._init(i),void this.ready(function(){v(n,i,e)}))};return i.VERSION="0.1.3",i};i.default=m},{"./lib/Detector":6,"./lib/Util":11,"iphone-inline-video":2}],14:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){return e.tech({IWillNotUseThisInPlugins:!0}).el()}function r(e){return e.controlBar.fullscreenToggle.handleClick}var s=e("./lib/Canvas"),o=n(s),h=e("./lib/ThreeCanvas"),l=n(h),c=e("./lib/Notice"),d=n(c),u=e("./lib/HelperCanvas"),f=n(u),v=e("./lib/VRButton"),p=n(v),m=e("./plugin"),g=n(m),b=videojs.getComponent("Component"),w=(0,d.default)(b);videojs.registerComponent("Notice",videojs.extend(b,w));var M=(0,f.default)(b);videojs.registerComponent("HelperCanvas",videojs.extend(b,M));var y=videojs.getComponent("Button"),T=(0,p.default)(y);videojs.registerComponent("VRButton",videojs.extend(y,T)),videojs.plugin("panorama",(0,g.default)({_init:function(e){var t="3dVideo"!==e.videoType?(0,o.default)(b,window.THREE,{getTech:a}):(0,l.default)(b,window.THREE,{getTech:a});videojs.registerComponent("Canvas",videojs.extend(b,t))},mergeOption:function(e,t){return videojs.mergeOptions(e,t)},getTech:a,getFullscreenToggleClickFn:r}))},{"./lib/Canvas":5,"./lib/HelperCanvas":7,"./lib/Notice":9,"./lib/ThreeCanvas":10,"./lib/VRButton":12,"./plugin":13}]},{},[14]); \ No newline at end of file +!function e(t,i,n){function a(s,o){if(!i[s]){if(!t[s]){var h="function"==typeof require&&require;if(!o&&h)return h(s,!0);if(r)return r(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var c=i[s]={exports:{}};t[s][0].call(c.exports,function(e){var i=t[s][1][e];return a(i?i:e)},c,c.exports,e,t,i,n)}return i[s].exports}for(var r="function"==typeof require&&require,s=0;s=e.video.duration}function d(e){var t=this;t.video.readyState>=t.video.HAVE_FUTURE_DATA?(t.hasAudio||(t.driver.currentTime=t.video.currentTime+e*t.video.playbackRate/1e3,t.video.loop&&c(t)&&(t.driver.currentTime=0)),l(t.video,t.driver.currentTime)):t.video.networkState!==t.video.NETWORK_IDLE||t.video.buffered.length||t.video.load(),t.video.ended&&(delete t.video[T],t.video.pause(!0))}function u(){var e=this,t=e[y];return e.webkitDisplayingFullscreen?void e[x]():("data:"!==t.driver.src&&t.driver.src!==e.src&&(l(e,0,!0),t.driver.src=e.src),void(e.paused&&(t.paused=!1,e.buffered.length||e.load(),t.driver.play(),t.updater.start(),t.hasAudio||(o(e,"play"),t.video.readyState>=t.video.HAVE_ENOUGH_DATA&&o(e,"playing")))))}function f(e){var t=this,i=t[y];i.driver.pause(),i.updater.stop(),t.webkitDisplayingFullscreen&&t[L](),i.paused&&!e||(i.paused=!0,i.hasAudio||o(t,"pause"),t.ended&&(t[T]=!0,o(t,"ended")))}function v(e,t){var i=e[y]={};i.paused=!0,i.hasAudio=t,i.video=e,i.updater=w.frameIntervalometer(d.bind(i)),t?i.driver=h(e):(e.addEventListener("canplay",function(){e.paused||o(e,"playing")}),i.driver={src:e.src||e.currentSrc||"data:",muted:!0,paused:!0,pause:function(){i.driver.paused=!0},play:function(){i.driver.paused=!1,c(i)&&l(e,0)},get ended(){return c(i)}}),e.addEventListener("emptied",function(){var t=!i.driver.src||"data:"===i.driver.src;i.driver.src&&i.driver.src!==e.src&&(l(e,0,!0),i.driver.src=e.src,t?i.driver.play():i.updater.stop())},!1),e.addEventListener("webkitbeginfullscreen",function(){e.paused?t&&!i.driver.buffered.length&&i.driver.load():(e.pause(),e[x]())}),t&&(e.addEventListener("webkitendfullscreen",function(){i.driver.currentTime=e.currentTime}),e.addEventListener("seeking",function(){R.indexOf(100*e.currentTime|0)<0&&(i.driver.currentTime=e.currentTime)}))}function p(e){var t=e[y];e[x]=e.play,e[L]=e.pause,e.play=u,e.pause=f,r(e,"paused",t.driver),r(e,"muted",t.driver,!0),r(e,"playbackRate",t.driver,!0),r(e,"ended",t.driver),r(e,"loop",t.driver,!0),a(e,"seeking"),a(e,"seeked"),a(e,"timeupdate",T,!1),a(e,"ended",T,!1)}function m(e,t,i){void 0===t&&(t=!0),void 0===i&&(i=!0),i&&!M||e[y]||(v(e,t),p(e),e.classList.add("IIV"),!t&&e.autoplay&&e.play(),/iPhone|iPod|iPad/.test(navigator.platform)||console.warn("iphone-inline-video is not guaranteed to work in emulated environments"))}var g,b=n(e("poor-mans-symbol")),w=e("intervalometer"),M=/iPhone|iPod/i.test(navigator.userAgent)&&!matchMedia("(-webkit-video-playable-inline)").matches,y=b(),T=b(),x=b("nativeplay"),L=b("nativepause"),R=[],C=0;m.isWhitelisted=M,t.exports=m},{intervalometer:1,"poor-mans-symbol":3}],3:[function(e,t,i){"use strict";var n="undefined"==typeof Symbol?function(e){return"@"+(e||"@")+Math.random()}:Symbol;t.exports=n},{}],4:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(i,"__esModule",{value:!0});var a=e("../lib/Detector"),r=n(a),s=e("../lib/MobileBuffering"),o=n(s),h=e("../lib/Util"),l=n(h),c=2,d=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return{constructor:function(n,a){this.settings=a,this.width=n.el().offsetWidth,this.height=n.el().offsetHeight,this.lon=a.initLon,this.lat=a.initLat,this.phi=0,this.theta=0,this.videoType=a.videoType,this.clickToToggle=a.clickToToggle,this.mouseDown=!1,this.isUserInteracting=!1,this.renderer=new t.WebGLRenderer,this.renderer.setPixelRatio(window.devicePixelRatio),this.renderer.setSize(this.width,this.height),this.renderer.autoClear=!1,this.renderer.setClearColor(0,1);var s=i.getTech(n);if(this.supportVideoTexture=r.default.supportVideoTexture(),this.liveStreamOnSafari=r.default.isLiveStreamOnSafari(s),this.liveStreamOnSafari&&(this.supportVideoTexture=!1),this.supportVideoTexture)this.texture=new t.Texture(s);else{this.helperCanvas=n.addChild("HelperCanvas",{video:s,width:a.helperCanvas.width?a.helperCanvas.width:this.width,height:a.helperCanvas.height?a.helperCanvas.height:this.height});var o=this.helperCanvas.el();this.texture=new t.Texture(o)}s.style.display="none",this.texture.generateMipmaps=!1,this.texture.minFilter=t.LinearFilter,this.texture.maxFilter=t.LinearFilter,this.texture.format=t.RGBFormat,this.el_=this.renderer.domElement,this.el_.classList.add("vjs-video-canvas"),a.el=this.el_,e.call(this,n,a),this.attachControlEvents(),this.player().on("play",function(){this.time=(new Date).getTime(),this.animate()}.bind(this))},attachControlEvents:function(){this.on("mousemove",this.handleMouseMove.bind(this)),this.on("touchmove",this.handleTouchMove.bind(this)),this.on("mousedown",this.handleMouseDown.bind(this)),this.on("touchstart",this.handleTouchStart.bind(this)),this.on("mouseup",this.handleMouseUp.bind(this)),this.on("touchend",this.handleTouchEnd.bind(this)),this.settings.scrollable&&(this.on("mousewheel",this.handleMouseWheel.bind(this)),this.on("MozMousePixelScroll",this.handleMouseWheel.bind(this))),this.on("mouseenter",this.handleMouseEnter.bind(this)),this.on("mouseleave",this.handleMouseLease.bind(this))},handleResize:function(){this.width=this.player().el().offsetWidth,this.height=this.player().el().offsetHeight,this.renderer.setSize(this.width,this.height)},handleMouseUp:function(e){if(this.mouseDown=!1,this.clickToToggle){var t=e.clientX||e.changedTouches&&e.changedTouches[0].clientX,i=e.clientY||e.changedTouches&&e.changedTouches[0].clientY;if("undefined"==typeof t||"undefined"===i)return;var n=Math.abs(t-this.onPointerDownPointerX),a=Math.abs(i-this.onPointerDownPointerY);n<.1&&a<.1&&(this.player().paused()?this.player().play():this.player().pause())}},handleMouseDown:function(e){e.preventDefault();var t=e.clientX||e.touches&&e.touches[0].clientX,i=e.clientY||e.touches&&e.touches[0].clientY;"undefined"!=typeof t&&"undefined"!==i&&(this.mouseDown=!0,this.onPointerDownPointerX=t,this.onPointerDownPointerY=i,this.onPointerDownLon=this.lon,this.onPointerDownLat=this.lat)},handleTouchStart:function(e){e.touches.length>1&&(this.isUserPinch=!0,this.multiTouchDistance=l.default.getTouchesDistance(e.touches)),this.handleMouseDown(e)},handleTouchEnd:function(e){this.isUserPinch=!1,this.handleMouseUp(e)},handleMouseMove:function(e){var t=e.clientX||e.touches&&e.touches[0].clientX,i=e.clientY||e.touches&&e.touches[0].clientY;if("undefined"!=typeof t&&"undefined"!==i)if(this.settings.clickAndDrag)this.mouseDown&&(this.lon=.2*(this.onPointerDownPointerX-t)+this.onPointerDownLon,this.lat=.2*(i-this.onPointerDownPointerY)+this.onPointerDownLat);else{var n=e.pageX-this.el_.offsetLeft,a=e.pageY-this.el_.offsetTop;this.lon=n/this.width*430-225,this.lat=a/this.height*-180+90}},handleTouchMove:function(e){(!this.isUserPinch||e.touches.length<=1)&&this.handleMouseMove(e)},handleMobileOrientation:function(e){if("undefined"!=typeof e.rotationRate){var t=e.rotationRate.alpha,i=e.rotationRate.beta,n="undefined"!=typeof e.portrait?e.portrait:window.matchMedia("(orientation: portrait)").matches,a="undefined"!=typeof e.landscape?e.landscape:window.matchMedia("(orientation: landscape)").matches,r=e.orientation||window.orientation;if(n)this.lon=this.lon-i*this.settings.mobileVibrationValue,this.lat=this.lat+t*this.settings.mobileVibrationValue;else if(a){var s=-90;"undefined"!=typeof r&&(s=r),this.lon=s==-90?this.lon+t*this.settings.mobileVibrationValue:this.lon-t*this.settings.mobileVibrationValue,this.lat=s==-90?this.lat+i*this.settings.mobileVibrationValue:this.lat-i*this.settings.mobileVibrationValue}}},handleMouseWheel:function(e){e.stopPropagation(),e.preventDefault()},handleMouseEnter:function(e){this.isUserInteracting=!0},handleMouseLease:function(e){this.isUserInteracting=!1,this.mouseDown&&(this.mouseDown=!1)},animate:function(){if(this.requestAnimationId=requestAnimationFrame(this.animate.bind(this)),!this.player().paused()&&"undefined"!=typeof this.texture&&(!this.isPlayOnMobile&&this.player().readyState()>=c||this.isPlayOnMobile&&this.player().hasClass("vjs-playing"))){var e=(new Date).getTime();if(e-this.time>=30&&(this.texture.needsUpdate=!0,this.time=e),this.isPlayOnMobile){var t=this.player().currentTime();o.default.isBuffering(t)?this.player().hasClass("vjs-panorama-mobile-inline-video-buffering")||this.player().addClass("vjs-panorama-mobile-inline-video-buffering"):this.player().hasClass("vjs-panorama-mobile-inline-video-buffering")&&this.player().removeClass("vjs-panorama-mobile-inline-video-buffering")}}this.render()},render:function(){if(!this.isUserInteracting){var e=this.lat>this.settings.initLat?-1:1,i=this.lon>this.settings.initLon?-1:1;this.settings.backToVerticalCenter&&(this.lat=this.lat>this.settings.initLat-Math.abs(this.settings.returnStepLat)&&this.latthis.settings.initLon-Math.abs(this.settings.returnStepLon)&&this.lon2&&void 0!==arguments[2]?arguments[2]:{},n=(0,r.default)(e,t,i);return o.default.extend(n,{constructor:function(e,i){n.constructor.call(this,e,i),this.VRMode=!1,this.scene=new t.Scene,this.camera=new t.PerspectiveCamera(i.initFov,this.width/this.height,1,2e3),this.camera.target=new t.Vector3(0,0,0);var a="equirectangular"===this.videoType?new t.SphereGeometry(500,60,40):new t.SphereBufferGeometry(500,60,40).toNonIndexed();if("fisheye"===this.videoType){for(var r=a.attributes.normal.array,s=a.attributes.uv.array,o=0,h=r.length/3;o=13},isLiveStreamOnSafari:function(e){for(var t=e.querySelectorAll("source"),i=!1,n=0;nWebGL.
','Find out how to get it here.'].join("\n"):['Your browser does not seem to support WebGL.
','Find out how to get it here.'].join("\n")),e},addGetWebGLMessage:function(e){var t,i,a;e=e||{},t=void 0!==e.parent?e.parent:document.body,i=void 0!==e.id?e.id:"oldie",a=n.getWebGLErrorMessage(),a.id=i,t.appendChild(a)}};i.default=n},{}],7:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n=document.createElement("canvas");n.className="vjs-video-helper-canvas";var a=function(e){return{constructor:function(t,i){this.videoElement=i.video,this.width=i.width,this.height=i.height,n.width=this.width,n.height=this.height,n.style.display="none",i.el=n,this.context=n.getContext("2d"),this.context.drawImage(this.videoElement,0,0,this.width,this.height),e.call(this,t,i)},getContext:function(){return this.context},update:function(){this.context.drawImage(this.videoElement,0,0,this.width,this.height)},el:function(){return n}}};i.default=a},{}],8:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n={prev_currentTime:0,counter:0,isBuffering:function(e){return e==this.prev_currentTime?this.counter++:this.counter=0,this.prev_currentTime=e,this.counter>10&&(this.counter=10,!0)}};i.default=n},{}],9:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a=function(e){var t=document.createElement("div");return t.className="vjs-video-notice-label",{constructor:function(i,a){"object"==n(a.NoticeMessage)?(t=a.NoticeMessage,a.el=a.NoticeMessage):"string"==typeof a.NoticeMessage&&(t.innerHTML=a.NoticeMessage,a.el=t),e.call(this,i,a)},el:function(){return t}}};i.default=a},{}],10:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(i,"__esModule",{value:!0});var a=e("./BaseCanvas"),r=n(a),s=e("./Util"),o=n(s),h=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=(0,r.default)(e,t,i);return o.default.extend(n,{constructor:function(e,i){n.constructor.call(this,e,i),this.VRMode=!1,this.scene=new t.Scene;var a=this.width/this.height;this.cameraL=new t.PerspectiveCamera(i.initFov,a,1,2e3),this.cameraL.target=new t.Vector3(0,0,0),this.cameraR=new t.PerspectiveCamera(i.initFov,a/2,1,2e3),this.cameraR.position.set(1e3,0,0),this.cameraR.target=new t.Vector3(1e3,0,0);for(var r=new t.SphereBufferGeometry(500,60,40).toNonIndexed(),s=new t.SphereBufferGeometry(500,60,40).toNonIndexed(),o=r.attributes.uv.array,h=r.attributes.normal.array,l=0;l1&&void 0!==arguments[1]?arguments[1]:{};for(var i in e)e.hasOwnProperty(i)&&!t.hasOwnProperty(i)&&(t[i]=e[i]);return t}function d(e){var t={};for(var i in e)t[i]=e[i];return t}function u(e){return Math.sqrt((e[0].clientX-e[1].clientX)*(e[0].clientX-e[1].clientX)+(e[0].clientY-e[1].clientY)*(e[0].clientY-e[1].clientY))}Object.defineProperty(i,"__esModule",{value:!0}),i.default={whichTransitionEvent:n,mobileAndTabletcheck:a,isIos:r,isRealIphone:s,fovToProjection:l,extend:c,deepCopy:d,getTouchesDistance:u}},{}],12:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n=function(e){return{constructor:function(t,i){e.call(this,t,i)},buildCSSClass:function(){return"vjs-VR-control "+e.prototype.buildCSSClass.call(this)},handleClick:function(){var e=this.player().getChild("Canvas");e.VRMode?e.disableVR():e.enableVR(),e.VRMode?this.addClass("enable"):this.removeClass("enable"),e.VRMode?this.player().trigger("VRModeOn"):this.player().trigger("VRModeOff")},controlText_:"VR"}};i.default=n},{}],13:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=e.getChild("Canvas");return function(){e.el().style.width=window.innerWidth+"px",e.el().style.height=window.innerHeight+"px",t.handleResize()}}function r(e,t){var i=a(e);e.controlBar.fullscreenToggle.off("tap",t),e.controlBar.fullscreenToggle.on("tap",function(){var t=e.getChild("Canvas");e.isFullscreen()?(e.isFullscreen(!1),e.exitFullWindow(),e.el().style.width="",e.el().style.height="",t.handleResize(),window.removeEventListener("devicemotion",i)):(e.isFullscreen(!0),e.enterFullWindow(),i(),window.addEventListener("devicemotion",i))})}Object.defineProperty(i,"__esModule",{value:!0});var s=e("./lib/Util"),o=n(s),h=e("./lib/Detector"),l=n(h),c=e("iphone-inline-video"),d=n(c),u=o.default.mobileAndTabletcheck(),f={clickAndDrag:u,showNotice:!0,NoticeMessage:"Please use your mouse drag and drop the video.",autoHideNotice:3e3,scrollable:!0,initFov:75,maxFov:105,minFov:51,initLat:0,initLon:-180,returnStepLat:.5,returnStepLon:2,backToVerticalCenter:!u,backToHorizonCenter:!u,clickToToggle:!1,minLat:-85,maxLat:85,minLon:-(1/0),maxLon:1/0,videoType:"equirectangular",rotateX:0,rotateY:0,rotateZ:0,autoMobileOrientation:!1,mobileVibrationValue:o.default.isIos()?.022:1,VREnable:!0,VRGapDegree:2.5,closePanorama:!1,helperCanvas:{},dualFish:{width:1920,height:1080,circle1:{x:.240625,y:.553704,rx:.23333,ry:.43148,coverX:.913,coverY:.9},circle2:{x:.757292,y:.553704,rx:.232292,ry:.4296296,coverX:.913,coverY:.9308}}},v=function(e,t,i){if(e.addClass("vjs-panorama"),!l.default.webgl)return p(e,{NoticeMessage:l.default.getWebGLErrorMessage(),autoHideNotice:t.autoHideNotice}),void(t.callback&&t.callback());e.addChild("Canvas",o.default.deepCopy(t));var n=e.getChild("Canvas");if(u){var a=i.getTech(e);o.default.isRealIphone()&&(a.setAttribute("playsinline",""),(0,d.default)(a,!0)),o.default.isIos()&&r(e,i.getFullscreenToggleClickFn(e)),e.addClass("vjs-panorama-mobile-inline-video"),e.removeClass("vjs-using-native-controls"),n.playOnMobile()}t.showNotice&&e.on("playing",function(){p(e,o.default.deepCopy(t))}),t.VREnable&&e.controlBar.addChild("VRButton",{},e.controlBar.children().length-1),n.hide(),e.on("play",function(){n.show()}),e.on("fullscreenchange",function(){n.handleResize()}),t.callback&&t.callback()},p=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{NoticeMessage:""},i=e.addChild("Notice",t);t.autoHideNotice>0&&setTimeout(function(){i.addClass("vjs-video-notice-fadeOut");var e=o.default.whichTransitionEvent(),t=function t(){i.hide(),i.removeClass("vjs-video-notice-fadeOut"),i.off(e,t)};i.on(e,t)},t.autoHideNotice)},m=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=["equirectangular","fisheye","3dVideo","dual_fisheye"],i=function(i){var n=this;return e.mergeOption&&(i=e.mergeOption(f,i)),"undefined"==typeof e._init||"function"!=typeof e._init?void console.error("plugin must implement init function()."):(t.indexOf(i.videoType)==-1&&(i.videoType=f.videoType),e._init(i),void this.ready(function(){v(n,i,e)}))};return i.VERSION="0.1.3",i};i.default=m},{"./lib/Detector":6,"./lib/Util":11,"iphone-inline-video":2}],14:[function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){return e.tech({IWillNotUseThisInPlugins:!0}).el()}function r(e){return e.controlBar.fullscreenToggle.handleClick}var s=e("./lib/Canvas"),o=n(s),h=e("./lib/ThreeCanvas"),l=n(h),c=e("./lib/Notice"),d=n(c),u=e("./lib/HelperCanvas"),f=n(u),v=e("./lib/VRButton"),p=n(v),m=e("./plugin"),g=n(m),b=videojs.getComponent("Component"),w=(0,d.default)(b);videojs.registerComponent("Notice",videojs.extend(b,w));var M=(0,f.default)(b);videojs.registerComponent("HelperCanvas",videojs.extend(b,M));var y=videojs.getComponent("Button"),T=(0,p.default)(y);videojs.registerComponent("VRButton",videojs.extend(y,T)),videojs.registerPlugin("panorama",(0,g.default)({_init:function(e){var t="3dVideo"!==e.videoType?(0,o.default)(b,window.THREE,{getTech:a}):(0,l.default)(b,window.THREE,{getTech:a});videojs.registerComponent("Canvas",videojs.extend(b,t))},mergeOption:function(e,t){return videojs.mergeOptions(e,t)},getTech:a,getFullscreenToggleClickFn:r}))},{"./lib/Canvas":5,"./lib/HelperCanvas":7,"./lib/Notice":9,"./lib/ThreeCanvas":10,"./lib/VRButton":12,"./plugin":13}]},{},[14]); \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/js/videojs-vimeo.js b/java/com.sap.sailing.gwt.ui/js/videojs-vimeo.js index 568ef6c2e80..1028ebf13cb 100644 --- a/java/com.sap.sailing.gwt.ui/js/videojs-vimeo.js +++ b/java/com.sap.sailing.gwt.ui/js/videojs-vimeo.js @@ -731,7 +731,7 @@ /** * @module lib/embed */ - var oEmbedParameters = ['autopause', 'autoplay', 'background', 'byline', 'color', 'height', 'id', 'loop', 'maxheight', 'maxwidth', 'muted', 'playsinline', 'portrait', 'responsive', 'speed', 'title', 'transparent', 'url', 'width']; + var oEmbedParameters = ['dnt', 'autopause', 'autoplay', 'background', 'byline', 'color', 'height', 'id', 'loop', 'maxheight', 'maxwidth', 'muted', 'playsinline', 'portrait', 'responsive', 'speed', 'title', 'transparent', 'url', 'width']; /** * Get the 'data-vimeo'-prefixed attributes from an element as an object. * @@ -2433,7 +2433,8 @@ var Vimeo = function (_Tech) { url: this.options_.source.src, byline: false, portrait: false, - title: false + title: false, + dnt: true }; if (this.options_.autoplay) { @@ -2457,6 +2458,9 @@ var Vimeo = function (_Tech) { if (this.options_.color) { // vimeo is the only API on earth to reject hex color with leading # vimeoOptions.color = this.options_.color.replace(/^#/, ''); + } + if (this.options_.dnt) { + vimeoOptions.dnt = this.options_.dnt; } this._player = new _player2.default(this.el(), vimeoOptions); @@ -2520,7 +2524,7 @@ var Vimeo = function (_Tech) { }; Vimeo.prototype.createEl = function createEl() { - var div = _video2.default.createEl('div', { + var div = _video2.default.dom.createEl('div', { id: this.options_.techId }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java index 9294c437a50..4788503c7b9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java @@ -427,7 +427,7 @@ public abstract class AbstractTrackedRacesListComposite extends AbstractComposit userService.getUserManagementWriteService(), type, regatta -> regattaRefresher.fillRegattas(), stringMessages); actionsColumn.addAction(RegattaConfigImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - configACL::openACLDialog); + configACL::openDialog); actionsColumn.addAction(RegattaConfigImagesBarCell.ACTION_DELETE, DefaultActions.DELETE, this::removeAndUntrackRace); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatTableWrapper.java index bac40835091..f0d064984f5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatTableWrapper.java @@ -185,7 +185,7 @@ public class BoatTableWrapper> exte final EditACLDialog.DialogConfig configACL = EditACLDialog .create(userService.getUserManagementWriteService(), type, null, stringMessages); boatActionColumn.addAction(BoatConfigImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - configACL::openACLDialog); + configACL::openDialog); mainPanel.insert(filterField, 0); table.addColumnSortHandler(boatColumnListHandler); table.addColumn(boatNameColumn, stringMessages.name()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index 5b332f6daf9..852565c4a3d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -337,7 +337,7 @@ public class CompetitorTableWrapper configACL = EditACLDialog .create(userService.getUserManagementWriteService(), type, null, stringMessages); competitorActionColumn.addAction(CompetitorConfigImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - configACL::openACLDialog); + configACL::openDialog); mainPanel.insert(filterField, 0); table.addColumnSortHandler(competitorColumnListHandler); table.addColumn(competitorNameColumn, getStringMessages().name()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java index 51912c660f0..e1f5be0f6e0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java @@ -161,7 +161,7 @@ public class DeviceConfigurationListComposite extends Composite { actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_OWNERSHIP, DefaultActions.CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - u -> configACL.openACLDialog(u)); + u -> configACL.openDialog(u)); SecuredDTOOwnerColumn.configureOwnerColumns(table, columnSortHandler, stringMessages); table.addColumn(deviceConfigurationUUidColumn, stringMessages.id()); table.addColumn(actionColumn, stringMessages.actions()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index f5a8e044629..44b08c991bf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -16,8 +16,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.Consumer; - import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.cell.client.SafeHtmlCell; import com.google.gwt.cell.client.ValueUpdater; @@ -79,7 +77,6 @@ import com.sap.sse.security.ui.client.component.EditOwnershipDialog.DialogConfig import com.sap.sse.security.ui.client.component.SecuredDTOOwnerColumn; import com.sap.sse.security.ui.client.component.editacl.EditACLDialog; -/** /** * A composite showing the list of all sailing events * @author Frank Mittag (C5163974) @@ -317,24 +314,11 @@ public class EventListComposite extends Composite implements EventsRefresher, Le }); final DialogConfig config = EditOwnershipDialog.create(userService.getUserManagementWriteService(), EVENT, event -> fillEvents(), stringMessages); - actionsColumn.addAction(EventConfigImagesBarCell.ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, - // Explicitly using an anonymous inner class. Using a method reference caused problems with the GWT compiler (see bug 5269) - new Consumer() { - @Override - public void accept(EventDTO t) { - config.openOwnershipDialog(t); - } - }); + actionsColumn.addAction(EventConfigImagesBarCell.ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, config::openOwnershipDialog); final EditACLDialog.DialogConfig configACL = EditACLDialog.create( userService.getUserManagementWriteService(), EVENT, event -> fillEvents(), stringMessages); actionsColumn.addAction(EventConfigImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - // Explicitly using an anonymous inner class. Using a method reference caused problems with the GWT compiler (see bug 5269) - new Consumer() { - @Override - public void accept(EventDTO e) { - configACL.openACLDialog(e); - } - }); + configACL::openDialog); final MigrateGroupOwnershipDialog.DialogConfig migrateDialogConfig = MigrateGroupOwnershipDialog .create(userService.getUserManagementService(), (event, dto) -> { sailingServiceWrite.updateGroupOwnerForEventHierarchy(event.id, dto, new AsyncCallback() { @@ -350,13 +334,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le }); }); actionsColumn.addAction(EventConfigImagesBarCell.ACTION_MIGRATE_GROUP_OWNERSHIP_HIERARCHY, CHANGE_OWNERSHIP, - // Explicitly using an anonymous inner class. Using a method reference caused problems with the GWT compiler (see bug 5269) - new Consumer() { - @Override - public void accept(EventDTO t) { - migrateDialogConfig.openDialog(t); - } - }); + migrateDialogConfig::openDialog); eventNameColumn.setSortable(true); venueNameColumn.setSortable(true); isPublicColumn.setSortable(true); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionDeviceConfigurationsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionDeviceConfigurationsPanel.java index 7f40865c5bb..46c91fceea0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionDeviceConfigurationsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExpeditionDeviceConfigurationsPanel.java @@ -160,7 +160,7 @@ public class ExpeditionDeviceConfigurationsPanel extends FlowPanel { final EditACLDialog.DialogConfig configACL = EditACLDialog .create(userService.getUserManagementWriteService(), type, deviceConfiguration -> refresh(), stringMessages); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - deviceConfiguration -> configACL.openACLDialog(deviceConfiguration)); + deviceConfiguration -> configACL.openDialog(deviceConfiguration)); SecuredDTOOwnerColumn.configureOwnerColumns(allDeviceConfigurations, deviceConfigurationColumnListHandler, stringMessages); allDeviceConfigurations.addColumn(actionsColumn, stringMessages.actions()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java index cc881c3eeb0..59d56ace1f5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java @@ -241,7 +241,7 @@ public class IgtimiAccountsPanel extends FlowPanel { final EditACLDialog.DialogConfig configACL = EditACLDialog .create(userService.getUserManagementWriteService(), type, roleDefinition -> refresh(), stringMessages); roleActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - configACL::openACLDialog); + configACL::openDialog); table.addColumn(accountSelectionCheckboxColumn, accountSelectionCheckboxColumn.getHeader()); table.addColumn(accountNameColumn, stringMessages.name()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 4a724f262f5..ee97867d55c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -374,7 +374,7 @@ public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel new Consumer() { @Override public void accept(StrippedLeaderboardDTOWithSecurity t) { - configACL.openACLDialog(t); + configACL.openDialog(t); } }); leaderboardTable.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index eda7e2770ef..d3fd37ab2d9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -557,24 +556,12 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel userService.getUserManagementWriteService(), type, group -> leaderboardGroupsRefresher.fillLeaderboardGroups(), stringMessages); actionsColumn.addAction(LeaderboardGroupConfigImagesBarCell.ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, - // Explicitly using an anonymous inner class. Using a method reference caused problems with the GWT compiler (see bug 5269) - new Consumer() { - @Override - public void accept(LeaderboardGroupDTO e) { - config.openOwnershipDialog(e); - } - }); + e -> config.openOwnershipDialog(e)); final EditACLDialog.DialogConfig configACL = EditACLDialog.create( userService.getUserManagementWriteService(), type, group -> leaderboardGroupsRefresher.fillLeaderboardGroups(), stringMessages); actionsColumn.addAction(LeaderboardGroupConfigImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - // Explicitly using an anonymous inner class. Using a method reference caused problems with the GWT compiler (see bug 5269) - new Consumer() { - @Override - public void accept(LeaderboardGroupDTO e) { - configACL.openACLDialog(e); - } - }); + e -> configACL.openDialog(e)); final MigrateGroupOwnershipDialog.DialogConfig migrateDialogConfig = MigrateGroupOwnershipDialog .create(userService.getUserManagementService(), (lg, dto) -> { sailingServiceWrite.updateGroupOwnerForLeaderboardGroupHierarchy(lg.getId(), dto, @@ -591,13 +578,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel }); }); actionsColumn.addAction(EventConfigImagesBarCell.ACTION_MIGRATE_GROUP_OWNERSHIP_HIERARCHY, CHANGE_OWNERSHIP, - // Explicitly using an anonymous inner class. Using a method reference caused problems with the GWT compiler (see bug 5269) - new Consumer() { - @Override - public void accept(LeaderboardGroupDTO e) { - migrateDialogConfig.openDialog(e); - } - }); + e -> migrateDialogConfig.openDialog(e)); SelectionCheckboxColumn leaderboardTableSelectionColumn = new SelectionCheckboxColumn( tableResources.cellTableStyle().cellTableCheckboxSelected(), diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java index 859ccbc48f7..632c9cc51cc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java @@ -96,7 +96,7 @@ public class LocalServerManagementPanel extends SimplePanel { buttonPanel.addAction(stringMessages.actionChangeOwnership(), () -> permissionCheck.test(CHANGE_OWNERSHIP), () -> configOwner.openOwnershipDialog(currentServerInfo)); buttonPanel.addAction(stringMessages.actionChangeACL(), () -> permissionCheck.test(CHANGE_ACL), - () -> configACL.openACLDialog(currentServerInfo)); + () -> configACL.openDialog(currentServerInfo)); return buttonPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index d5a28bb379f..6f2c32eb6af 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -587,7 +587,7 @@ public class MediaPanel extends FlowPanel implements MediaTracksRefresher { stringMessages); actionsColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - mediaTrack -> configACL.openACLDialog(mediaTrack)); + mediaTrack -> configACL.openDialog(mediaTrack)); mediaTracksTable.addColumn(actionsColumn, stringMessages.actions()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index 3aa5e48b122..6f604ba676c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -293,7 +293,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer userService.getUserManagementWriteService(), type, regatta -> regattaRefresher.fillRegattas(), stringMessages); actionsColumn.addAction(RegattaConfigImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - regattaDTO -> configACL.openACLDialog(regattaDTO)); + regattaDTO -> configACL.openDialog(regattaDTO)); table.addColumn(regattaSelectionCheckboxColumn, regattaSelectionCheckboxColumn.getHeader()); table.addColumn(regattaNameColumn, stringMessages.regattaName()); table.addColumn(regattaCanBoatsOfCompetitorsChangePerRaceColumn, stringMessages.canBoatsChange()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsTableWrapper.java index 9413d04bd49..5f54e08758c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsTableWrapper.java @@ -62,7 +62,7 @@ public class ResultImportUrlsTableWrapper configACL = EditACLDialog.create(userService.getUserManagementWriteService(), SecuredDomainType.RESULT_IMPORT_URL, event -> update(), stringMessages); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - e -> configACL.openACLDialog(e)); + e -> configACL.openDialog(e)); super.table.addColumn(urlColumn, stringMessages.url()); SecuredDTOOwnerColumn.configureOwnerColumns(super.table, listHandler, stringMessages); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java index 3d7369937b9..5c65ec4deff 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java @@ -217,7 +217,7 @@ public class SmartphoneTrackingEventManagementPanel leaderboardActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_OWNERSHIP, DefaultActions.UPDATE, configOwnership::openOwnershipDialog); leaderboardActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.UPDATE, - configACL::openACLDialog); + configACL::openDialog); leaderboardTable.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); leaderboardTable.addColumn(leaderboardNameColumn, stringMessages.name()); leaderboardTable.addColumn(leaderboardDisplayNameColumn, stringMessages.displayName()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/CourseTemplatePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/CourseTemplatePanel.java index aa416034bbe..b94000c67a3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/CourseTemplatePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/CourseTemplatePanel.java @@ -331,7 +331,7 @@ public class CourseTemplatePanel extends FlowPanel { actionsColumn.addAction(ACTION_UPDATE, UPDATE, e -> openEditCourseTemplateDialog(e, userService, false)); actionsColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - courseTemplate -> configACL.openACLDialog(courseTemplate)); + courseTemplate -> configACL.openDialog(courseTemplate)); courseTemplateTable.addColumn(idColumn, stringMessages.id()); courseTemplateTable.addColumn(actionsColumn, stringMessages.actions()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkPropertiesPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkPropertiesPanel.java index d7341edcabc..0ed3868ccaf 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkPropertiesPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkPropertiesPanel.java @@ -362,7 +362,7 @@ public class MarkPropertiesPanel extends FlowPanel { this::unsetPosition); actionsColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - markProperties -> configACL.openACLDialog(markProperties)); + markProperties -> configACL.openDialog(markProperties)); markPropertiesTable.addColumn(idColumn, stringMessages.id()); markPropertiesTable.addColumn(actionsColumn, stringMessages.actions()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkRolePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkRolePanel.java index de4b90283a9..b0d20e1ab2d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkRolePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkRolePanel.java @@ -244,7 +244,7 @@ public class MarkRolePanel extends FlowPanel { markRole -> markRole.getAccessControlList(), stringMessages); actionsColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - markRole -> configACL.openACLDialog(markRole)); + markRole -> configACL.openDialog(markRole)); markRolesTable.addColumn(idColumn, stringMessages.id()); markRolesTable.addColumn(actionsColumn, stringMessages.actions()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkTemplatePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkTemplatePanel.java index 739f157d45f..1b724f1f357 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkTemplatePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/coursecreation/MarkTemplatePanel.java @@ -278,7 +278,7 @@ public class MarkTemplatePanel extends FlowPanel { stringMessages); actionsColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionsColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - markTemplate -> configACL.openACLDialog(markTemplate)); + markTemplate -> configACL.openDialog(markTemplate)); markTemplateTable.addColumn(idColumn, stringMessages.id()); markTemplateTable.addColumn(actionsColumn, stringMessages.actions()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingArchivedConnectionTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingArchivedConnectionTableWrapper.java index 33fcf32fd43..e7b8a1b457b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingArchivedConnectionTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingArchivedConnectionTableWrapper.java @@ -121,7 +121,7 @@ public class SwissTimingArchivedConnectionTableWrapper extends actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_OWNERSHIP, DefaultActions.CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - u -> configACL.openACLDialog(u)); + u -> configACL.openDialog(u)); filterField = new LabeledAbstractFilterablePanel( new Label(stringMessages.filterSwissTimingAchivedConnections()), new ArrayList(), dataProvider, stringMessages) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java index a1b4289940e..8d37f9e3205 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java @@ -138,7 +138,7 @@ public class SwissTimingConnectionTableWrapper extends actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_OWNERSHIP, DefaultActions.CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - u -> configACL.openACLDialog(u)); + u -> configACL.openDialog(u)); filterField = new LabeledAbstractFilterablePanel( new Label(stringMessagesClient.filterSwissTimingConnections()), diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java index fc64bcdee3a..a6b32a5f200 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java @@ -132,7 +132,7 @@ public class TracTracConnectionTableWrapper extends actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_OWNERSHIP, DefaultActions.CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - u -> configACL.openACLDialog(u)); + u -> configACL.openDialog(u)); filterField = new LabeledAbstractFilterablePanel( new Label(stringMessages.filterTracTracConnections()), new ArrayList(), dataProvider, stringMessages) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/NewMediaDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/NewMediaDialog.java index 8807eb6475d..bb2e7a0f8f1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/NewMediaDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/NewMediaDialog.java @@ -15,6 +15,8 @@ import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.media.client.Audio; import com.google.gwt.media.client.MediaBase; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.typedarrays.shared.Int8Array; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; @@ -203,6 +205,8 @@ public class NewMediaDialog extends DataEntryDialog implements FileS mediaTrack.startTime = defaultStartTime; } + + boolean isVimeoUrl = isVimeoUrl(url); String youtubeId = YoutubeApi.getIdByUrl(url); if (youtubeId != null) { mediaTrack.url = youtubeId; @@ -224,6 +228,9 @@ public class NewMediaDialog extends DataEntryDialog implements FileS } } }); + } else if (isVimeoUrl) { + mediaTrack.url = url; + mediaTrack.mimeType = MimeType.vimeo; } else { mediaTrack.url = url; AnchorElement anchor = Document.get().createAnchorElement(); @@ -250,6 +257,17 @@ public class NewMediaDialog extends DataEntryDialog implements FileS } } + private boolean isVimeoUrl(String url) { + try { + RegExp urlPattern = RegExp.compile("^(.*:)//([A-Za-z0-9\\-\\.]+)(:[0-9]+)?(.*)$"); + MatchResult matchResult = urlPattern.exec(url); + String host = matchResult.getGroup(2); + return host.contains("vimeo.com"); + } catch (Exception e) { + return false; + } + } + //used for audio only tracks, using native mediaelement to determine time private void loadMediaDuration() { MediaBase mediaBase = Audio.createIfSupported(); @@ -336,12 +354,15 @@ public class NewMediaDialog extends DataEntryDialog implements FileS if (!remoteMp4WasStarted) { processMp4(mediaTrack); } else if (remoteMp4WasFinished) { - manualMimeTypeSelection(null, mediaTrack); + manualMimeTypeSelection(null, mediaTrack, MimeType.mp4MimeTypes()); } else { infoLabel.setWidget(new Label(stringMessages.processingMP4())); } + } else if (mediaTrack.mimeType == MimeType.vimeo) { + infoLabel.setWidget(new Label(mediaTrack.typeToString())); } else { - infoLabel.setWidget(new Label(mediaTrack.typeToString())); + manualMimeTypeSelection(null, mediaTrack, new MimeType[] { MimeType.mp4, MimeType.mp4panorama, + MimeType.mp4panoramaflip, MimeType.youtube, MimeType.vimeo }); } } startTimeBox.setValue(mediaTrack.startTime == null ? null : mediaTrack.startTime.asDate(), DONT_FIRE_EVENTS); @@ -375,7 +396,7 @@ public class NewMediaDialog extends DataEntryDialog implements FileS public void onFailure(Throwable caught) { busyIndicator.setBusy(false); remoteMp4WasFinished = true; - manualMimeTypeSelection(caught.getMessage(), mediaTrack); + manualMimeTypeSelection(caught.getMessage(), mediaTrack, MimeType.mp4MimeTypes()); } }); } @@ -444,7 +465,7 @@ public class NewMediaDialog extends DataEntryDialog implements FileS protected void mp4MetadataResult(VideoMetadataDTO result) { if (!result.isDownloadable()) { Notification.notify(stringMessages.couldNotDownload(mediaTrack.url), NotificationType.ERROR); - manualMimeTypeSelection(result.getMessage(), mediaTrack); + manualMimeTypeSelection(result.getMessage(), mediaTrack, MimeType.mp4MimeTypes()); } else { mediaTrack.duration = result.getDuration(); if (mediaTrack.duration == null) { @@ -459,15 +480,15 @@ public class NewMediaDialog extends DataEntryDialog implements FileS } } - private void manualMimeTypeSelection(String message, MediaTrack mediaTrack) { + private void manualMimeTypeSelection(String message, MediaTrack mediaTrack, MimeType[] proposedMimeTypes) { FlowPanel fp = new FlowPanel(); if (message != null) { fp.add(new Label(message)); } ListBox mimeTypeListBox = createListBox(false); - mimeTypeListBox.addItem(MimeType.mp4.name()); - mimeTypeListBox.addItem(MimeType.mp4panorama.name()); - mimeTypeListBox.addItem(MimeType.mp4panoramaflip.name()); + for (int i = 0; i < proposedMimeTypes.length; i++) { + mimeTypeListBox.addItem(proposedMimeTypes[i].name()); + } mimeTypeListBox.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.java index c34cd415a53..1d9a830ad4d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.java @@ -143,8 +143,11 @@ public class VideoJSPlayer extends Widget implements RequiresResize { "customControlsOnMobile" : true, youtube: { enablePrivacyEnhancedMode: true, - customVars: { rel: 0, modestbranding: 1 } - } + customVars: { rel: 0, modestbranding: 1 } + }, + vimeo: { + "dnt": 1, + } }); player.ready(function() { this.on('play', function() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.ui.xml index 9d0222a4ac9..9985f817b4d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSPlayer.ui.xml @@ -8,7 +8,6 @@ .player { width: 100%; height: 100%; - min-height: 230px; } .invertedVideoPlayer canvas{ diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 2162fb15277..20116e03abf 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3762,8 +3762,7 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet } private VideoDTO convertToVideoDTO(VideoDescriptor video) { - String videoRef = convertToNoCookieUrlWhereApplicable(video.getURL(), video.getMimeType()); - VideoDTO result = new VideoDTO(videoRef, video.getMimeType(), + VideoDTO result = new VideoDTO(video.getURL().toString(), video.getMimeType(), video.getCreatedAtDate() != null ? video.getCreatedAtDate().asDate() : null); result.setCopyright(video.getCopyright()); result.setTitle(video.getTitle()); @@ -3778,23 +3777,7 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet result.setTags(tags); return result; } - - private String convertToNoCookieUrlWhereApplicable(URL videoUrl, MimeType mimeType) { - String result = videoUrl.toString(); - switch (mimeType) { - case youtube: - result.replace("youtube", "youtube-nocookies"); - break; - case vimeo: - result += videoUrl.getQuery() == null ? "?" : "&"; - result += "dnt=true"; - break; - default: - break; - } - return result; - } - + //READ private Locale toLocale(String localeName) { if(localeName == null || localeName.isEmpty()) { diff --git a/java/com.sap.sailing.landscape.test/.classpath b/java/com.sap.sailing.landscape.test/.classpath new file mode 100644 index 00000000000..3e5654f17eb --- /dev/null +++ b/java/com.sap.sailing.landscape.test/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/java/com.sap.sailing.landscape.test/.project b/java/com.sap.sailing.landscape.test/.project new file mode 100644 index 00000000000..1f13eb0413e --- /dev/null +++ b/java/com.sap.sailing.landscape.test/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.landscape.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.landscape.test/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.landscape.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..9f6ece88bdf --- /dev/null +++ b/java/com.sap.sailing.landscape.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.landscape.test/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.landscape.test/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..f29e940a005 --- /dev/null +++ b/java/com.sap.sailing.landscape.test/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.landscape.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.landscape.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..2d1bbcbdea4 --- /dev/null +++ b/java/com.sap.sailing.landscape.test/META-INF/MANIFEST.MF @@ -0,0 +1,10 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: com.sap.sailing.landscape.test +Bundle-SymbolicName: com.sap.sailing.landscape.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Fragment-Host: com.sap.sailing.landscape +Automatic-Module-Name: com.sap.sailing.landscape.test +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.junit diff --git a/java/com.sap.sailing.landscape.test/build.properties b/java/com.sap.sailing.landscape.test/build.properties new file mode 100644 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.landscape.test/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.landscape.test/pom.xml b/java/com.sap.sailing.landscape.test/pom.xml new file mode 100644 index 00000000000..0fe1e29dd8f --- /dev/null +++ b/java/com.sap.sailing.landscape.test/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.landscape.test + + eclipse-plugin + diff --git a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java new file mode 100644 index 00000000000..5aeeee7fd97 --- /dev/null +++ b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java @@ -0,0 +1,95 @@ +package com.sap.sailing.landscape.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Optional; +import java.util.Random; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.landscape.SailingAnalyticsHost; +import com.sap.sailing.landscape.SailingAnalyticsMaster; +import com.sap.sailing.landscape.SailingAnalyticsMetrics; +import com.sap.sailing.landscape.SailingAnalyticsReplica; +import com.sap.sailing.landscape.procedures.StartSailingAnalyticsMaster; +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.application.ApplicationProcess; +import com.sap.sse.landscape.application.impl.ApplicationProcessImpl; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.Tags; +import com.sap.sse.landscape.aws.impl.AwsRegion; + +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceType; +import software.amazon.awssdk.services.ec2.model.Tag; + +/** + * Tests for the AWS SDK landscape wrapper in bundle {@code com.sap.sse.landscape.aws}. To run these tests + * successfully it is necessary to have valid AWS credentials for region {@code EU_WEST_2} that allow the + * AWS user account to create keys and launch instances, etc. These are to be provided as explained + * in the documentation of {@link AwsLandscape#obtain()}. + * + * @author Axel Uhl (D043530) + * + */ +public class TestProcedures { + private static final Optional optionalTimeout = Optional.of(Duration.ONE_MINUTE.times(5)); + private AwsLandscape, SailingAnalyticsReplica> landscape; + private AwsRegion region; + + @Before + public void setUp() { + landscape = AwsLandscape.obtain(); + region = new AwsRegion(Region.EU_WEST_2); + } + + @Test + public void testConnectivity() throws Exception { + final String serverName = "test"+new Random().nextInt(); + final String keyName = "MyKey-"+UUID.randomUUID(); + final StartSailingAnalyticsMaster.Builder builder = StartSailingAnalyticsMaster.builder(); + final StartSailingAnalyticsMaster startSailingAnalyticsMaster = builder + .setServerName(serverName) + .setLandscape(landscape) + .setRegion(region) + .setInstanceType(InstanceType.T3_SMALL) + .setKeyName(keyName) + .setCommaSeparatedEmailAddressesToNotifyOfStartup("axel.uhl@sap.com") + .setTags(Optional.of(Tags.with("Hello", "World"))) + .setOptionalTimeout(optionalTimeout) + .build(); + startSailingAnalyticsMaster.run(); + final SailingAnalyticsHost host = startSailingAnalyticsMaster.getHost(); + try { + assertNotNull(host); + final Instance instance = landscape.getInstance(host.getInstanceId(), region); + boolean foundName = false; + boolean foundHello = false; + for (final Tag tag : instance.tags()) { + if (tag.key().equals("Name") && tag.value().equals("MyHost")) { + foundName = true; + } + if (tag.key().equals("Hello") && tag.value().equals("World")) { + foundHello = true; + } + } + assertTrue(foundName); + assertTrue(foundHello); + // check env.sh access + final ApplicationProcess, SailingAnalyticsReplica> process = new ApplicationProcessImpl<>(8888, host, "/home/sailing/servers/server"); + final String envSh = process.getEnvSh(optionalTimeout); + assertFalse(envSh.isEmpty()); + assertTrue(envSh.contains("SERVER_NAME="+serverName)); + assertEquals(14888, process.getTelnetPortToOSGiConsole(optionalTimeout)); + } finally { + landscape.terminate(host); + landscape.deleteKeyPair(region, keyName); + } + } +} diff --git a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java new file mode 100644 index 00000000000..97c09a8e51e --- /dev/null +++ b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java @@ -0,0 +1,22 @@ +package com.sap.sailing.landscape.test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +import com.sap.sailing.landscape.SailingReleaseRepository; +import com.sap.sse.common.Util; + +public class TestReleaseRepository { + + @Test + public void testForAtLeastOneRelease() { + assertFalse(Util.isEmpty(SailingReleaseRepository.INSTANCE)); + } + + @Test + public void testForAtLeastOneMasterRelease() { + assertNotNull(SailingReleaseRepository.INSTANCE.getLatestMasterRelease()); + } +} diff --git a/java/com.sap.sailing.landscape/META-INF/MANIFEST.MF b/java/com.sap.sailing.landscape/META-INF/MANIFEST.MF index 79a4b3db8b8..5f8b6478119 100755 --- a/java/com.sap.sailing.landscape/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.landscape/META-INF/MANIFEST.MF @@ -9,4 +9,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Require-Bundle: com.sap.sse.landscape, com.sap.sse.landscape.aws, com.sap.sse.common, - com.jcraft.jsch;bundle-version="0.1.54" + com.jcraft.jsch;bundle-version="0.1.54", + com.amazon.aws.aws-java-api;bundle-version="2.14.24", + com.sap.sse.shared.android, + org.apache.httpcomponents.httpcore;bundle-version="4.4.0" +Export-Package: com.sap.sailing.landscape diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsHost.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsHost.java index 20485e9f569..b776809a9cd 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsHost.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsHost.java @@ -1,21 +1,21 @@ package com.sap.sailing.landscape; -import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.RotatingFileBasedLog; import com.sap.sse.landscape.application.Scope; import com.sap.sse.landscape.aws.AwsInstance; import com.sap.sse.landscape.aws.ReverseProxy; -public interface SailingAnalyticsHost extends AwsInstance { +public interface SailingAnalyticsHost extends AwsInstance { /** * Obtains an object through which an Apache reverse proxy running on this sailing analytics host can be configured. * It is mainly used to decide how to route based on a URL's hostname or other {@link Scope} identification, and to * expand base URLs to, e.g., the URL of a specific event in that scope or an overview page of an event series or * simply the home/landing page. It furthermore handles logging in a consistent way. */ - ReverseProxy, SailingAnalyticsReplica> getReverseProxy(); + ReverseProxy, SailingAnalyticsReplica, RotatingFileBasedLog> getReverseProxy(); /** * Obtains the Sailing Analytics processes running on this host. Can be zero or more. */ - Iterable> getSailingAnalyticsProcesses(); + Iterable> getSailingAnalyticsProcesses(); } diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMaster.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMaster.java index bde14f702f4..a6157e9956f 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMaster.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMaster.java @@ -1,9 +1,8 @@ package com.sap.sailing.landscape; import com.sap.sse.landscape.application.ApplicationMasterProcess; -import com.sap.sse.landscape.application.ApplicationProcessMetrics; -public interface SailingAnalyticsMaster extends SailingAnalyticsProcess, -ApplicationMasterProcess, SailingAnalyticsReplica> { +public interface SailingAnalyticsMaster extends SailingAnalyticsProcess, +ApplicationMasterProcess, SailingAnalyticsReplica> { } diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMetrics.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMetrics.java new file mode 100644 index 00000000000..8f24cb140cb --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsMetrics.java @@ -0,0 +1,7 @@ +package com.sap.sailing.landscape; + +import com.sap.sse.landscape.application.ApplicationProcessMetrics; + +public interface SailingAnalyticsMetrics extends ApplicationProcessMetrics { + +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java index 4084fac9df0..c529961710f 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java @@ -1,10 +1,14 @@ package com.sap.sailing.landscape; -import com.sap.sse.landscape.application.ApplicationProcess; -import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import java.io.IOException; +import java.util.Optional; -public interface SailingAnalyticsProcess extends - ApplicationProcess, SailingAnalyticsReplica> { - int getTelnetPort(); - int getExpeditionUdpPort(); +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.application.ApplicationProcess; + +public interface SailingAnalyticsProcess extends + ApplicationProcess, SailingAnalyticsReplica> { + int getExpeditionUdpPort(Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, InterruptedException, SftpException; } diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcessConfigurationVariable.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcessConfigurationVariable.java new file mode 100644 index 00000000000..ea440fe1bcc --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcessConfigurationVariable.java @@ -0,0 +1,8 @@ +package com.sap.sailing.landscape; + +public enum SailingAnalyticsProcessConfigurationVariable { + /** + * The user data variable that specifies the UDP port the application listens to + */ + EXPEDITION_PORT; +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsReplica.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsReplica.java index 2e4b862a959..07bab39a1c5 100755 --- a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsReplica.java +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsReplica.java @@ -1,9 +1,8 @@ package com.sap.sailing.landscape; -import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.application.ApplicationReplicaProcess; -public interface SailingAnalyticsReplica extends SailingAnalyticsProcess, -ApplicationReplicaProcess, SailingAnalyticsReplica> { +public interface SailingAnalyticsReplica extends SailingAnalyticsProcess, +ApplicationReplicaProcess, SailingAnalyticsReplica> { } diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingReleaseRepository.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingReleaseRepository.java new file mode 100644 index 00000000000..de233dca09b --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingReleaseRepository.java @@ -0,0 +1,8 @@ +package com.sap.sailing.landscape; + +import com.sap.sse.landscape.ReleaseRepository; +import com.sap.sse.landscape.impl.ReleaseRepositoryImpl; + +public interface SailingReleaseRepository extends ReleaseRepository { + ReleaseRepository INSTANCE = new ReleaseRepositoryImpl("http://releases.sapsailing.com", "build"); +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/BearerTokenReplicationCredentials.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/BearerTokenReplicationCredentials.java new file mode 100644 index 00000000000..55ae796a7a5 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/BearerTokenReplicationCredentials.java @@ -0,0 +1,23 @@ +package com.sap.sailing.landscape.impl; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sse.landscape.ProcessConfigurationVariable; +import com.sap.sse.landscape.ReplicationCredentials; + +public class BearerTokenReplicationCredentials implements ReplicationCredentials { + private final String bearerToken; + + public BearerTokenReplicationCredentials(String bearerToken) { + this.bearerToken = bearerToken; + } + + @Override + public Map getUserData() { + final Map result = new HashMap<>(); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_BEARER_TOKEN, bearerToken); + return result; + } + +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsHostImpl.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsHostImpl.java new file mode 100644 index 00000000000..a62e79849db --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsHostImpl.java @@ -0,0 +1,32 @@ +package com.sap.sailing.landscape.impl; + +import com.sap.sailing.landscape.SailingAnalyticsHost; +import com.sap.sailing.landscape.SailingAnalyticsMaster; +import com.sap.sailing.landscape.SailingAnalyticsMetrics; +import com.sap.sailing.landscape.SailingAnalyticsProcess; +import com.sap.sailing.landscape.SailingAnalyticsReplica; +import com.sap.sse.landscape.RotatingFileBasedLog; +import com.sap.sse.landscape.aws.AwsAvailabilityZone; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.ReverseProxy; +import com.sap.sse.landscape.aws.impl.AwsInstanceImpl; + +public class SailingAnalyticsHostImpl extends AwsInstanceImpl implements SailingAnalyticsHost { + public SailingAnalyticsHostImpl(String instanceId, AwsAvailabilityZone availabilityZone, + AwsLandscape landscape) { + super(instanceId, availabilityZone, landscape); + // TODO Auto-generated constructor stub + } + + @Override + public ReverseProxy, SailingAnalyticsReplica, RotatingFileBasedLog> getReverseProxy() { + // TODO Implement SailingAnalyticsHost.getReverseProxy(...) + return null; + } + + @Override + public Iterable> getSailingAnalyticsProcesses() { + // TODO Implement SailingAnalyticsHost.getSailingAnalyticsProcesses(...) + return null; + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java new file mode 100644 index 00000000000..b96cfe437fe --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java @@ -0,0 +1,47 @@ +package com.sap.sailing.landscape.impl; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Optional; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; +import com.sap.sailing.landscape.SailingAnalyticsMaster; +import com.sap.sailing.landscape.SailingAnalyticsMetrics; +import com.sap.sailing.landscape.SailingAnalyticsProcess; +import com.sap.sailing.landscape.SailingAnalyticsProcessConfigurationVariable; +import com.sap.sailing.landscape.SailingAnalyticsReplica; +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.application.impl.ApplicationProcessImpl; + +public class SailingAnalyticsProcessImpl extends ApplicationProcessImpl, SailingAnalyticsReplica> implements SailingAnalyticsProcess { + private static final String HEALTH_CHECK_PATH = "/gwt/status"; + + public SailingAnalyticsProcessImpl(int port, Host host) { + super(port, host, "/home/sailing/servers/server"); + } + + @Override + public String getHealthCheckPath() { + return HEALTH_CHECK_PATH; + } + + private URL getHealthCheckUrl(Optional optionalTimeout) throws MalformedURLException { + return new URL("http", getHost().getPublicAddress(optionalTimeout).getCanonicalHostName(), getPort(), getHealthCheckPath()); + } + + @Override + public boolean isReady(Optional optionalTimeout) throws IOException { + final HttpURLConnection connection = (HttpURLConnection) getHealthCheckUrl(optionalTimeout).openConnection(); + return connection.getResponseCode() == 200; + } + + @Override + public int getExpeditionUdpPort(Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, InterruptedException, SftpException { + return Integer.parseInt(getEnvShValueFor(getEnvSh(optionalTimeout), SailingAnalyticsProcessConfigurationVariable.EXPEDITION_PORT.name(), optionalTimeout)); + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/UsernamePasswordReplicationCredentials.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/UsernamePasswordReplicationCredentials.java new file mode 100644 index 00000000000..1b8048a57b9 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/UsernamePasswordReplicationCredentials.java @@ -0,0 +1,25 @@ +package com.sap.sailing.landscape.impl; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sse.landscape.ProcessConfigurationVariable; +import com.sap.sse.landscape.ReplicationCredentials; + +public class UsernamePasswordReplicationCredentials implements ReplicationCredentials { + private final String username; + private final String password; + + public UsernamePasswordReplicationCredentials(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public Map getUserData() { + final Map result = new HashMap<>(); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_USERNAME, username); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_PASSWORD, password); + return result; + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java new file mode 100644 index 00000000000..8684fd72319 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/DeployProcessOnMultiServer.java @@ -0,0 +1,75 @@ +package com.sap.sailing.landscape.procedures; + +import com.sap.sailing.landscape.SailingAnalyticsHost; +import com.sap.sailing.landscape.SailingAnalyticsMaster; +import com.sap.sailing.landscape.SailingAnalyticsMetrics; +import com.sap.sailing.landscape.SailingAnalyticsProcess; +import com.sap.sailing.landscape.SailingAnalyticsReplica; +import com.sap.sse.landscape.Landscape; +import com.sap.sse.landscape.orchestration.Procedure; + +/** + * Deploys a single {@link SailingAnalyticsProcess} to a given {@link SailingAnalyticsHost} which ideally has been + * launched using the {@link StartMultiServer} procedure, but may also work for hosts started in another way.

+ * + * TODO This is similar to {@link StartSailingAnalyticsHost} but doesn't need to fire up a new host. The assembly + * of user data that {@link StartSailingAnalyticsHost} implements shall be factored and used here as well to append + * to a barebones {@code env.sh} as obtained from a trivial installation. This trivial installation would start by + * creating the {@link #serverName} folder under {@code /home/sailing/servers}, then copy the {@code refreshInstance.sh} + * script there, run {@code ./refreshInstance.sh install-release}, then {@code ./refreshInstance.sh install-env environment-name}, + * then append specific variable values to the end of the {@code env.sh} file. In both cases, a set of variable assignments + * needs to be generated which becomes the "user data" for {@link StartSailingAnalyticsHost} and becomes an appendix to + * {@code env.sh} in this case. + * + * @author Axel Uhl (D043530) + * + * @param + * @param + */ +public class DeployProcessOnMultiServer> implements + Procedure, SailingAnalyticsReplica> { + private final Landscape, SailingAnalyticsReplica> landscape; + private final SailingAnalyticsHost hostToDeployTo; + private final String serverName; + + /** + * The process launched by this procedure. {@link #hostToDeployTo} is expected to be identical to + * {@link SailingAnalyticsProcess#getHost() process.getHost()}. + */ + private SailingAnalyticsProcess process; + + public DeployProcessOnMultiServer(Landscape, SailingAnalyticsReplica> landscape, + SailingAnalyticsHost hostToDeployTo, String serverName) { + super(); + this.landscape = landscape; + this.hostToDeployTo = hostToDeployTo; + this.serverName = serverName; + } + + @Override + public void run() { + // TODO Implement Runnable.run(...) + + } + + public SailingAnalyticsProcess getProcess() { + return process; + } + + public void setProcess(SailingAnalyticsProcess process) { + this.process = process; + } + + public SailingAnalyticsHost getHostToDeployTo() { + return hostToDeployTo; + } + + public String getServerName() { + return serverName; + } + + @Override + public Landscape, SailingAnalyticsReplica> getLandscape() { + return landscape; + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java new file mode 100644 index 00000000000..dbbe3f75764 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartMultiServer.java @@ -0,0 +1,91 @@ +package com.sap.sailing.landscape.procedures; + +import java.io.ByteArrayOutputStream; +import java.util.Optional; +import java.util.logging.Logger; + +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.ssh.SshCommandChannel; + +import software.amazon.awssdk.services.ec2.model.InstanceType; + +/** + * Starts an empty multi-server. The image will cause a {@code /home/sailing/servers/server} directory to exist, but + * after successfully launching, that directory will be removed. A {@link DeployProcessOnMultiServer} procedure needs to + * be run with the {@link #getHost()} of this procedure telling the host on which to deploy the process.

+ * + * The implementation specializes the {@link UpgradeAmi} procedure in {@link Builder#setNoShutdown(boolean) no-shutdown} mode. + * After running that part, the {@code httpd} service is launched.

+ * + * You want to at least specify an {@link Builder#setInstanceName(String) instance name} and {@link Builder#setInstanceType(InstanceType)}. + * + * @author Axel Uhl (D043530) + * + * @param + * @param + */ +public class StartMultiServer, +ReplicaProcessT extends ApplicationReplicaProcess, +HostT extends AwsInstance> +extends UpgradeAmi { + private static final Logger logger = Logger.getLogger(StartMultiServer.class.getName()); + private Optional optionalTimeout; + + /** + * Under all circumstances, this builder will return {@code true} for {@link #isNoShutdown()}, making sure + * that after the upgrade progress the server does not try to re-boot. + * + * @author Axel Uhl (D043530) + */ + public static interface Builder, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> + extends UpgradeAmi.Builder { + @Override + default boolean isNoShutdown() { + return true; + } + } + + protected static class BuilderImpl, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> + extends UpgradeAmi.BuilderImpl + implements Builder { + @Override + public StartMultiServer build() { + return new StartMultiServer<>(this); + } + } + + public static , + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> Builder builder() { + return new BuilderImpl<>(); + } + + protected StartMultiServer(Builder builder) { + super(builder); + this.optionalTimeout = builder.getOptionalTimeout(); + } + + @Override + public void run() throws Exception { + super.run(); + final SshCommandChannel sshCommandChannel = getHost().createRootSshChannel(optionalTimeout); + final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + sshCommandChannel.sendCommandLineSynchronously("service httpd start", stderr); + final String instanceId = getHost().getInstanceId(); + logger.info("stdout for starting httpd service on instance "+instanceId+": "+sshCommandChannel.getStreamContentsAsString()); + logger.info("stderr for starting httpd service on instance \"+instanceId+\": "+stderr.toString()); + logger.info("exit status for starting httpd service on instance \"+instanceId+\": "+sshCommandChannel.getExitStatus()); + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java new file mode 100644 index 00000000000..eb4fc2afa77 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsHost.java @@ -0,0 +1,73 @@ +package com.sap.sailing.landscape.procedures; + +import java.util.Optional; + +import com.sap.sailing.landscape.SailingAnalyticsHost; +import com.sap.sailing.landscape.SailingAnalyticsMaster; +import com.sap.sailing.landscape.SailingAnalyticsMetrics; +import com.sap.sailing.landscape.SailingAnalyticsReplica; +import com.sap.sailing.landscape.SailingReleaseRepository; +import com.sap.sse.landscape.Release; +import com.sap.sse.landscape.aws.impl.AmazonMachineImage; +import com.sap.sse.landscape.aws.orchestration.StartAwsHost; +import com.sap.sse.landscape.orchestration.Procedure; + +/** + * TODO handle the naming problem: base name drives instance "Name" tag generation ("SL ... (Master)"), exchange name, + * database name and SERVER_NAME. When moving up the inheritance hierarchy, name is interpreted in some places as the instance name + * which obviously doesn't equal the "Name" tag value. So, we have to clearly distinguish these. + * + * @author Axel Uhl (D043530) + * + * @param + */ +public abstract class StartSailingAnalyticsHost> +extends StartAwsHost, SailingAnalyticsReplica, HostT> +implements Procedure, SailingAnalyticsReplica> { + private final static String IMAGE_TYPE_TAG_VALUE_SAILING = "sailing-analytics-server"; + private final static String INSTANCE_NAME_DEFAULT_PREFIX = "SL "; + + /** + * The following defaults, in addition to the defaults implemented by the more general {@link StartAwsHost.Builder}, + * are: + *

    + *
  • If no {@link #setInstanceName(String) instance name} is provided, the instance name is constructed from the {@link #getServerName() server name} + * by pre-pending the prefix "SL ".
  • + *
  • Uses the latest machine image of the type described by + * {@link StartSailingAnalyticsHost#IMAGE_TYPE_TAG_VALUE_SAILING} if no explicit {@link #setMachineImage(AmazonMachineImage) machine image is set} + * and no {@link #setImageType(String) image type is set} of which the latest version would be used otherwise.
  • + *
  • If no {@link Release} is explicitly {@link #setRelease set}, or that {@link Optional} is empty, + * {@link SailingReleaseRepository#INSTANCE}{@link SailingReleaseRepository#getLatestMasterRelease() + * getLatestMasterRelease()} will be used instead.
  • + *
+ * + * @author Axel Uhl (D043530) + */ + public static interface Builder, ShardingKey, HostT extends SailingAnalyticsHost> + extends StartAwsHost.Builder, SailingAnalyticsReplica, HostT> { + } + + protected abstract static class BuilderImpl, ShardingKey, HostT extends SailingAnalyticsHost> + extends StartAwsHost.BuilderImpl, SailingAnalyticsReplica, HostT> + implements Builder { + @Override + public String getImageType() { + return super.getImageType() == null ? StartSailingAnalyticsHost.IMAGE_TYPE_TAG_VALUE_SAILING : super.getImageType(); + } + + @Override + public Optional getRelease() { + return Optional.of(super.getRelease().orElse(SailingReleaseRepository.INSTANCE.getLatestMasterRelease())); + } + + @Override + public String getInstanceName() { + return super.getInstanceName() == null ? INSTANCE_NAME_DEFAULT_PREFIX+getServerName() : super.getInstanceName(); + } + } + + protected StartSailingAnalyticsHost(Builder, ShardingKey, HostT> builder) { + super(builder); + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsMaster.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsMaster.java new file mode 100644 index 00000000000..b9ce06b4427 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsMaster.java @@ -0,0 +1,44 @@ +package com.sap.sailing.landscape.procedures; + +import com.sap.sailing.landscape.SailingAnalyticsHost; +import com.sap.sse.landscape.ProcessConfigurationVariable; + +/** + * This procedure does two things: it {@link StartSailingAnalyticsHost starts} a {@link SailingAnalyticsHost}, and + * (currently implicitly, based on the way the /etc/init.d/sailing script works) also starts a "master" process + * that is expected to have the default working directory {@code /home/sailing/servers/server}.

+ * + * TODO What we should probably be doing instead is harmonize the way the set-up / launching of a regular default master + * works with how a {@link StartMultiServer multi-server is started}. We could start both empty, with only the default + * reverse proxy mappings for {@code internal-server-status} and the plain access through the {@code ec2-...} hostname. + * From there on, all process launching and stopping would work through the {@link DeployProcessOnMultiServer} procedure + * (which then should be renamed to {@code DeployApplicationProcessOnServer}). That procedure then would have the + * + * @author Axel Uhl (D043530) + */ +public class StartSailingAnalyticsMaster + extends StartSailingAnalyticsHost> { + public static interface Builder + extends StartSailingAnalyticsHost.Builder, ShardingKey, SailingAnalyticsHost> { + } + + // TODO model an AwsLandscape subclass describing the specifics of the Sailing landscape, with a central security service that a master replicates by default + + protected static class BuilderImpl + extends StartSailingAnalyticsHost.BuilderImpl, ShardingKey, SailingAnalyticsHost> + implements Builder { + @Override + public StartSailingAnalyticsMaster build() { + return new StartSailingAnalyticsMaster(this); + } + } + + public static Builder builder() { + return new BuilderImpl<>(); + } + + protected StartSailingAnalyticsMaster(Builder builder) { + super(builder); + addUserData(ProcessConfigurationVariable.USE_ENVIRONMENT, "live-master-server"); // TODO maybe this should be handled by this procedure adding the correct defaults, e.g., for replicating security/sharedsailing? + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsReplica.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsReplica.java new file mode 100644 index 00000000000..d2b12e64191 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/StartSailingAnalyticsReplica.java @@ -0,0 +1,29 @@ +package com.sap.sailing.landscape.procedures; + +import com.sap.sailing.landscape.SailingAnalyticsHost; +import com.sap.sse.landscape.ProcessConfigurationVariable; + +public class StartSailingAnalyticsReplica + extends StartSailingAnalyticsHost> { + public static interface Builder + extends StartSailingAnalyticsHost.Builder, ShardingKey, SailingAnalyticsHost> { + } + + protected static class BuilderImpl + extends StartSailingAnalyticsHost.BuilderImpl, ShardingKey, SailingAnalyticsHost> + implements Builder { + @Override + public StartSailingAnalyticsReplica build() { + return new StartSailingAnalyticsReplica(this); + } + } + + public static Builder builder() { + return new BuilderImpl<>(); + } + + protected StartSailingAnalyticsReplica(Builder builder) { + super(builder); + addUserData(ProcessConfigurationVariable.USE_ENVIRONMENT, "live-replica-server"); // TODO maybe this should be handled by this procedure adding the correct defaults, e.g., for replicating security/sharedsailing? + } +} diff --git a/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java new file mode 100644 index 00000000000..7f407ae2e91 --- /dev/null +++ b/java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/procedures/UpgradeAmi.java @@ -0,0 +1,114 @@ +package com.sap.sailing.landscape.procedures; + +import java.util.Collections; + +import com.sap.sse.landscape.MachineImage; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.aws.orchestration.StartAwsHost; +import com.sap.sse.landscape.orchestration.Procedure; + +/** + * Upgrades an existing Amazon Machine Image that is expected to be prepared for such an upgrade, by + * invoking it with very specific user data that trigger the automatic upgrade. The resulting AMI can + * be obtained after this procedure has completed by calling {@link #getUpgradedAmi()}. + * + * @author Axel Uhl (D043530) + * + * @param + * @param + */ +public class UpgradeAmi, +ReplicaProcessT extends ApplicationReplicaProcess, +HostT extends AwsInstance> +extends StartAwsHost +implements Procedure { + private static final String IMAGE_UPGRADE_USER_DATA = "image-upgrade"; + private static final String NO_SHUTDOWN_USER_DATA = "no-shutdown"; + + private MachineImage upgradedAmi; + + /** + * Additional default rules in addition to what the {@link StartAwsHost.Builder parent builder} defines: + * + *

    + *
  • If no {@link #getInstanceName() instance name} is set, the default instance name will be constructed as + * {@code IMAGE_UPGRADE+" for "+machineImage.getId()}
  • + *
  • The user data are set to the string defined by {@link UpgradeAmi#IMAGE_UPGRADE_USER_DATA}, forcing the image to + * boot without trying to launch a process instance.
  • + *
+ * @author Axel Uhl (D043530) + */ + public static interface Builder, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> + extends StartAwsHost.Builder, ShardingKey, MetricsT, MasterProcessT, ReplicaProcessT, HostT> { + boolean isNoShutdown(); + + Builder setNoShutdown(boolean noShutdown); + } + + protected static class BuilderImpl, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> + extends StartAwsHost.BuilderImpl, ShardingKey, MetricsT, MasterProcessT, ReplicaProcessT, HostT> + implements Builder { + private boolean noShutdown; + + @Override + public UpgradeAmi build() { + return new UpgradeAmi<>(this); + } + + @Override + public boolean isNoShutdown() { + return noShutdown; + } + + @Override + public Builder setNoShutdown(boolean noShutdown) { + this.noShutdown = noShutdown; + return this; + } + + @Override + public String getInstanceName() { + return super.getInstanceName() == null ? IMAGE_UPGRADE_USER_DATA+" for "+getMachineImage().getId() : super.getInstanceName(); + } + } + + public static , + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> Builder builder() { + return new BuilderImpl<>(); + } + + public UpgradeAmi(Builder builder) { + super(builder); + addUserData(Collections.singleton(IMAGE_UPGRADE_USER_DATA)); + if (builder.isNoShutdown()) { + addUserData(Collections.singleton(NO_SHUTDOWN_USER_DATA)); + } + } + + @Override + public void run() throws Exception { + super.run(); // launches the machine in upgrade mode and shuts it down again, preparing for AMI creation + // TODO now comes the waiting for the shutdown and initiating the creation of an AMI for the instance + // TODO then comes the tagging of the volume snapshots created + // TODO then tag the resulting AMI according to the original image's tags, except for the name where automatic version number increment should be implemented + } + + /** + * @return the resulting AMI that has the upgraded version of everything + */ + public MachineImage getUpgradedAmi() { + return upgradedAmi; + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MongoClientURITest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MongoClientURITest.java index 39385c06732..ecf88b9c1bb 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MongoClientURITest.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MongoClientURITest.java @@ -25,5 +25,6 @@ public class MongoClientURITest { assertEquals(12345, config.getPort()); assertEquals("mydb", config.getDatabaseName()); assertSame(ReadPreference.primary(), config.getMongoClientURI().getOptions().getReadPreference()); + assertEquals("rs0", config.getMongoClientURI().getOptions().getRequiredReplicaSetName()); } } diff --git a/java/com.sap.sailing.polars/META-INF/MANIFEST.MF b/java/com.sap.sailing.polars/META-INF/MANIFEST.MF index 38c0ee75115..76e6fd4fe6b 100644 --- a/java/com.sap.sailing.polars/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.polars/META-INF/MANIFEST.MF @@ -41,9 +41,9 @@ Import-Package: javax.ws.rs.core;version="1.1.1", javax.xml.bind, org.apache.http, - org.apache.http.client;version="[4.2.6,4.3.0)", - org.apache.http.client.methods;version="[4.2.6,4.3.0)", - org.apache.http.impl.client;version="[4.2.6,4.3.0)", + org.apache.http.client;version="4.5.5", + org.apache.http.client.methods;version="4.5.5", + org.apache.http.impl.client;version="4.5.5", org.json.simple, org.json.simple.parser Web-ContextPath: /polars diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java index 9137d1e28b7..dec7b2a56f5 100644 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java @@ -8,7 +8,7 @@ import java.util.logging.Logger; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.json.simple.parser.ParseException; import com.sap.sailing.polars.ReplicablePolarService; @@ -69,7 +69,7 @@ public class PolarDataClient { } protected InputStream getContentFromResponse() throws IOException, ParseException { - HttpClient client = new SystemDefaultHttpClient(); + HttpClient client = HttpClientBuilder.create().build(); HttpGet getProcessor = new HttpGet(getAPIString()); HttpResponse processorResponse = client.execute(getProcessor); return processorResponse.getEntity().getContent(); diff --git a/java/com.sap.sailing.selenium.test/.gitignore b/java/com.sap.sailing.selenium.test/.gitignore index ae3c1726048..e2b7ea322ea 100644 --- a/java/com.sap.sailing.selenium.test/.gitignore +++ b/java/com.sap.sailing.selenium.test/.gitignore @@ -1 +1,2 @@ /bin/ +/debug.log diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java index 6d7b28431f1..223972cc9bf 100755 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java @@ -718,16 +718,12 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource { @QueryParam(RaceLogServletConstants.PARAMS_RACE_FLEET_NAME) String fleetName, @QueryParam(RaceLogServletConstants.PARAMS_TRACK_WIND) Boolean trackWind, @QueryParam(RaceLogServletConstants.PARAMS_CORRECT_WIND_DIRECTION_BY_MAGNETIC_DECLINATION) Boolean correctWindDirectionByMagneticDeclination, - @QueryParam(RaceLogServletConstants.PARAMS_TRACKED_RACE_NAME) String optionalTrackedRaceName, - @QueryParam("secret") String secret) + @QueryParam(RaceLogServletConstants.PARAMS_TRACKED_RACE_NAME) String optionalTrackedRaceName) throws NotDenotedForRaceLogTrackingException, Exception { final LeaderboardAndRaceColumnAndFleetAndResponse leaderboardAndRaceColumnAndFleetAndResponse = getLeaderboardAndRaceColumnAndFleet( leaderboardName, raceColumnName, fleetName); - boolean skip = getService().skipChecksDueToCorrectSecret(leaderboardName, secret); - if (!skip) { - SecurityUtils.getSubject().checkPermission(SecuredDomainType.LEADERBOARD.getStringPermissionForObject( - DefaultActions.UPDATE, leaderboardAndRaceColumnAndFleetAndResponse.getLeaderboard())); - } + SecurityUtils.getSubject().checkPermission(SecuredDomainType.LEADERBOARD.getStringPermissionForObject( + DefaultActions.UPDATE, leaderboardAndRaceColumnAndFleetAndResponse.getLeaderboard())); final Callable innerAction = () -> { final Response result; if (leaderboardAndRaceColumnAndFleetAndResponse.getFleet() != null) { diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/LeaderboardWithEliminationTransitiveRemovalTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/LeaderboardWithEliminationTransitiveRemovalTest.java new file mode 100644 index 00000000000..426042ede98 --- /dev/null +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/LeaderboardWithEliminationTransitiveRemovalTest.java @@ -0,0 +1,88 @@ +package com.sap.sailing.server.test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.CompetitorRegistrationType; +import com.sap.sailing.domain.common.RankingMetrics; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.DelegatingRegattaLeaderboardWithCompetitorElimination; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.persistence.PersistenceFactory; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.interfaces.RacingEventService; +import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; +import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; +import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboardWithEliminations; +import com.sap.sailing.server.operationaltransformation.RemoveLeaderboard; +import com.sap.sailing.server.operationaltransformation.RemoveRegatta; +import com.sap.sse.security.SecurityService; +import com.sap.sse.security.testsupport.SecurityServiceMockFactory; + +public class LeaderboardWithEliminationTransitiveRemovalTest { + private RacingEventService server; + private Regatta regatta; + private RegattaLeaderboard regattaLeaderboard; + private DelegatingRegattaLeaderboardWithCompetitorElimination regattaLeaderboardWithEliminations; + + @Before + public void setUp() { + PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().getDatabase().drop(); + final SecurityService securityService = SecurityServiceMockFactory.mockSecurityService(); + server = new RacingEventServiceImpl() { + @Override + public SecurityService getSecurityService() { + return securityService; + } + }; + final LinkedHashMap seriesStructure = new LinkedHashMap<>(); + final RegattaCreationParametersDTO regattaStructure = new RegattaCreationParametersDTO(seriesStructure); + regatta = server.apply(new AddSpecificRegatta("Test", "Laser Int.", /* canBoatsOfCompetitorsChangePerRace */ false, + /* competitorRegistrationType */ CompetitorRegistrationType.CLOSED, /* registrationLinkSecret */ null, + /* startDate */ null, /* endDate */ null, /* id */ UUID.randomUUID(), + regattaStructure, /* persistent */ true, new LowPoint(), + /* courseAreaIds */ Collections.singleton(server.getBaseDomainFactory().getOrCreateCourseArea(UUID.randomUUID(), "Default").getId()), + /* buoyZoneRadiusInHullLengths */ null, /* useStartTimeInference */ false, + /* controlTrackingFromStartAndFinishTimes */ true, /* autoRestartTrackingUponCompetitorSetChange */ false, + RankingMetrics.ONE_DESIGN)); + regattaLeaderboard = server.apply(new CreateRegattaLeaderboard(regatta.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); + regattaLeaderboardWithEliminations = + server.apply(new CreateRegattaLeaderboardWithEliminations("With Eliminations", /* display name */ null, + regattaLeaderboard.getName())); + } + + @Test + public void testAllThere() { + assertNotNull(regatta); + assertNotNull(regattaLeaderboard); + assertNotNull(regattaLeaderboardWithEliminations); + assertSame(regatta, regattaLeaderboard.getRegatta()); + assertSame(regatta, regattaLeaderboardWithEliminations.getRegatta()); + } + + @Test + public void testRemovingRegattaRemovesAllLeaderboards() { + server.apply(new RemoveRegatta(regatta.getRegattaIdentifier())); + assertNull(server.getLeaderboardByName(regattaLeaderboard.getName())); + assertNull(server.getLeaderboardByName(regattaLeaderboardWithEliminations.getName())); + } + + @Test + public void testRemovingRegattaLeaderboardRemovesLeaderboardWithEliminations() { + server.apply(new RemoveLeaderboard(regattaLeaderboard.getName())); + assertNull(server.getLeaderboardByName(regattaLeaderboard.getName())); + assertNull(server.getLeaderboardByName(regattaLeaderboardWithEliminations.getName())); + } +} diff --git a/java/com.sap.sailing.simulator/META-INF/MANIFEST.MF b/java/com.sap.sailing.simulator/META-INF/MANIFEST.MF index e43f2468896..6acadf2b352 100644 --- a/java/com.sap.sailing.simulator/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.simulator/META-INF/MANIFEST.MF @@ -10,8 +10,8 @@ Require-Bundle: com.sap.sailing.domain.common;bundle-version="1.0.0", com.sap.sailing.domain.tractracadapter, com.sap.sse, com.sap.sse.common, - com.sap.sse.security.common, - org.apache.httpcomponents.httpclient;bundle-version="[4.2.6,4.3.0)", + com.sap.sse.security.common, + org.apache.httpcomponents.httpclient;bundle-version="4.5.5", org.json.simple;bundle-version="1.1.0" Bundle-ActivationPolicy: lazy Import-Package: org.osgi.framework;version="1.6.0" diff --git a/java/com.sap.sailing.targetplatform.base/features/target-base/feature.xml b/java/com.sap.sailing.targetplatform.base/features/target-base/feature.xml index 5633f7133d3..5b76ac1a73d 100755 --- a/java/com.sap.sailing.targetplatform.base/features/target-base/feature.xml +++ b/java/com.sap.sailing.targetplatform.base/features/target-base/feature.xml @@ -82,13 +82,6 @@ version="1.1.2" unpack="false"/> - - - + @@ -29,8 +29,8 @@ - - + + @@ -43,10 +43,11 @@ - - - - + + + + + @@ -120,7 +121,7 @@ - + @@ -166,6 +167,7 @@ + @@ -285,4 +287,4 @@ - + \ No newline at end of file diff --git a/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/ManeuverAndWindImporter.java b/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/ManeuverAndWindImporter.java index 0ca91010d4d..ae6a98d4ae1 100644 --- a/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/ManeuverAndWindImporter.java +++ b/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/ManeuverAndWindImporter.java @@ -22,11 +22,10 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.SystemDefaultHttpClient; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -102,13 +101,13 @@ public class ManeuverAndWindImporter { } public HttpClient createNewHttpClient() { - HttpParams httpParams = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParams, CONNECTION_TIMEOUT_MILLIS); - HttpClient client = new SystemDefaultHttpClient(httpParams); - client.getParams().setParameter("http.socket.timeout", CONNECTION_TIMEOUT_MILLIS); - client.getParams().setParameter("http.connection.timeout", CONNECTION_TIMEOUT_MILLIS); - client.getParams().setParameter("http.connection-manager.timeout", new Long(CONNECTION_TIMEOUT_MILLIS)); - client.getParams().setParameter("http.protocol.head-body-timeout", CONNECTION_TIMEOUT_MILLIS); + CloseableHttpClient client = HttpClientBuilder.create() + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(CONNECTION_TIMEOUT_MILLIS) + .setConnectionRequestTimeout(CONNECTION_TIMEOUT_MILLIS) + .setSocketTimeout(CONNECTION_TIMEOUT_MILLIS) + .build()) + .build(); return client; } diff --git a/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/PolarDataImporter.java b/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/PolarDataImporter.java index 4014ae1e2d4..77046dc9316 100644 --- a/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/PolarDataImporter.java +++ b/java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/importer/PolarDataImporter.java @@ -8,7 +8,7 @@ import org.apache.commons.io.FileUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.json.simple.parser.ParseException; import com.sap.sailing.windestimation.util.LoggingUtil; @@ -42,7 +42,7 @@ public class PolarDataImporter { } protected InputStream getContentFromResponse() throws IOException, ParseException { - HttpClient client = new SystemDefaultHttpClient(); + HttpClient client = HttpClientBuilder.create().build(); HttpGet getProcessor = new HttpGet(getAPIString()); HttpResponse processorResponse = client.execute(getProcessor); return processorResponse.getEntity().getContent(); diff --git a/java/com.sap.sailing.windestimation/META-INF/MANIFEST.MF b/java/com.sap.sailing.windestimation/META-INF/MANIFEST.MF index 1db9c22f449..b0ca0099db5 100644 --- a/java/com.sap.sailing.windestimation/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.windestimation/META-INF/MANIFEST.MF @@ -8,8 +8,8 @@ Bundle-Vendor: SAP Require-Bundle: org.eclipse.osgi;bundle-version="3.10.1", org.json.simple;bundle-version="1.1.0", org.mongodb.mongo-java-driver;bundle-version="3.6.4", - org.apache.httpcomponents.httpclient;bundle-version="[4.2.5,4.3.0)", - org.apache.httpcomponents.httpcore;bundle-version="[4.0.0,4.3.0)", + org.apache.httpcomponents.httpclient;bundle-version="4.5.5", + org.apache.httpcomponents.httpcore;bundle-version="4.4.9", com.sap.sse, com.sap.sse.common, com.sap.sailing.domain.shared.android, @@ -40,9 +40,9 @@ Import-Package: javax.ws.rs;version="1.1.1", javax.ws.rs.core;version="1.1.1", javax.xml.bind, org.apache.http, - org.apache.http.client;version="[4.2.6,4.3.0)", - org.apache.http.client.methods;version="[4.2.6,4.3.0)", - org.apache.http.impl.client;version="[4.2.6,4.3.0)" + org.apache.http.client;version="4.5.5", + org.apache.http.client.methods;version="4.5.5", + org.apache.http.impl.client;version="4.5.5" Web-ContextPath: /windestimation Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy diff --git a/java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/jaxrs/client/WindEstimationDataClient.java b/java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/jaxrs/client/WindEstimationDataClient.java index 1788be37c78..0c5b1f93ce1 100644 --- a/java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/jaxrs/client/WindEstimationDataClient.java +++ b/java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/jaxrs/client/WindEstimationDataClient.java @@ -8,7 +8,7 @@ import java.util.logging.Logger; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.json.simple.parser.ParseException; import com.sap.sailing.windestimation.integration.ReplicableWindEstimationFactoryService; @@ -61,7 +61,7 @@ public class WindEstimationDataClient { } protected InputStream getContentFromResponse() throws IOException, ParseException { - HttpClient client = new SystemDefaultHttpClient(); + HttpClient client = HttpClientBuilder.create().build(); HttpGet getProcessor = new HttpGet(getAPIString()); HttpResponse processorResponse = client.execute(getProcessor); return processorResponse.getEntity().getContent(); diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/MimeType.java b/java/com.sap.sse.common/src/com/sap/sse/common/media/MimeType.java index d837a2d69e8..8caf72ba0a8 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/MimeType.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/media/MimeType.java @@ -40,5 +40,9 @@ public enum MimeType { return null; } } + + public static MimeType[] mp4MimeTypes() { + return new MimeType[] { mp4, mp4panorama, mp4panoramaflip}; + } } \ No newline at end of file diff --git a/java/com.sap.sse.feature.runtime/feature.xml b/java/com.sap.sse.feature.runtime/feature.xml index 23205b68f5d..dab5bd85d0a 100644 --- a/java/com.sap.sse.feature.runtime/feature.xml +++ b/java/com.sap.sse.feature.runtime/feature.xml @@ -38,6 +38,13 @@ version="9.4.8.v20171121" unpack="false"/> + + + + optionalTimeout = Optional.of(Duration.ONE_MINUTE.times(5)); private AwsLandscape landscape; private AwsRegion region; private byte[] keyPass; @@ -59,13 +79,59 @@ public class ConnectivityTest { } @Test - public void testConnectivity() { + public , ReplicaT extends ApplicationReplicaProcess> + void testConnectivity() throws JSchException, IOException, SftpException, NumberFormatException, InterruptedException { + final String TARGET_GROUP_NAME_PREFIX = "S-test-"; + final String hostedZoneName = "wiesen-weg.de"; + final String hostname = "test-"+new Random().nextInt()+"."+hostedZoneName; + final String keyName = "MyKey-"+UUID.randomUUID(); + createKeyPair(keyName); final AwsInstance host = landscape.launchHost(landscape.getImage(region, "ami-01b4b27a5699e33e6"), - InstanceType.T3_SMALL, landscape.getAvailabilityZoneByName(region, "eu-west-2b"), "Axel", Collections.singleton(()->"sg-0b2afd48960251280")); + InstanceType.T3_SMALL, landscape.getAvailabilityZoneByName(region, "eu-west-2b"), keyName, Collections.singleton(()->"sg-0b2afd48960251280"), + Optional.of(Tags.with("Name", "MyHost").and("Hello", "World"))); try { assertNotNull(host); + final Instance instance = landscape.getInstance(host.getInstanceId(), region); + boolean foundName = false; + boolean foundHello = false; + for (final Tag tag : instance.tags()) { + if (tag.key().equals("Name") && tag.value().equals("MyHost")) { + foundName = true; + } + if (tag.key().equals("Hello") && tag.value().equals("World")) { + foundHello = true; + } + } + assertTrue(foundName); + assertTrue(foundHello); + // check env.sh access + final ApplicationProcess process = new ApplicationProcessImpl<>(8888, host, "/home/sailing/servers/server"); + final String envSh = process.getEnvSh(optionalTimeout); + assertFalse(envSh.isEmpty()); + assertTrue(envSh.contains("SERVER_NAME=")); + final Release release = process.getRelease(new ReleaseRepositoryImpl("http://releases.sapsailing.com", "build"), optionalTimeout); + assertNotNull(release); + assertEquals(14888, process.getTelnetPortToOSGiConsole(optionalTimeout)); + @SuppressWarnings("unchecked") + final AwsLandscape castLandscape = (AwsLandscape) landscape; + final CreateDNSBasedLoadBalancerMapping> createDNSBasedLoadBalancerMappingProcedure = + new CreateDNSBasedLoadBalancerMapping>( + process, hostname, TARGET_GROUP_NAME_PREFIX, castLandscape, optionalTimeout); + final String wiesenWegId = landscape.getDNSHostedZoneId(hostedZoneName); + try { + createDNSBasedLoadBalancerMappingProcedure.run(); + assertNotNull(createDNSBasedLoadBalancerMappingProcedure.getLoadBalancerUsed()); + assertNotNull(createDNSBasedLoadBalancerMappingProcedure.getMasterTargetGroupCreated()); + assertEquals(TARGET_GROUP_NAME_PREFIX+process.getServerName(optionalTimeout), createDNSBasedLoadBalancerMappingProcedure.getPublicTargetGroupCreated().getName()); + } finally { + if (createDNSBasedLoadBalancerMappingProcedure.getLoadBalancerUsed() != null) { + createDNSBasedLoadBalancerMappingProcedure.getLoadBalancerUsed().delete(); + landscape.removeDNSRecord(wiesenWegId, hostname, RRType.CNAME, createDNSBasedLoadBalancerMappingProcedure.getLoadBalancerUsed().getDNSName()); + } + } } finally { landscape.terminate(host); + landscape.deleteKeyPair(region, keyName); } } @@ -131,31 +197,27 @@ public class ConnectivityTest { private void testSshConnectWithKey(final String keyName) throws InterruptedException, JSchException, IOException { final AwsInstance host = landscape.launchHost(landscape.getImage(region, "ami-01b4b27a5699e33e6"), - InstanceType.T3_SMALL, landscape.getAvailabilityZoneByName(region, "eu-west-2b"), keyName, Collections.singleton(()->"sg-0b2afd48960251280")); + InstanceType.T3_SMALL, landscape.getAvailabilityZoneByName(region, "eu-west-2b"), keyName, Collections.singleton(()->"sg-0b2afd48960251280"), /* tags */ Optional.empty()); try { assertNotNull(host); logger.info("Created instance with ID "+host.getInstanceId()); logger.info("Waiting for public IP address..."); // wait for public IPv4 address to become available: - InetAddress address = host.getPublicAddress(Duration.ONE_SECOND.times(20)); + InetAddress address = host.getPublicAddress(optionalTimeout); assertNotNull(address); logger.info("Obtained public IP address "+address); - SshCommandChannel shellChannel = null; - int sshConnectAttempts = 20; - while (shellChannel == null && sshConnectAttempts-- > 0) { - try { - shellChannel = host.createRootSshChannel(); - } catch (JSchException e) { - logger.info(e.getMessage() - + " while trying to connect. Probably timeout trying early SSH connection. Retrying " - + sshConnectAttempts + " more times..."); - Thread.sleep(10000); - } - } + SshCommandChannel shellChannel = host.createRootSshChannel(optionalTimeout); assertNotNull(shellChannel); logger.info("Shell channel connected. Waiting for it to become responsive..."); shellChannel.sendCommandLineSynchronously("pwd", System.err); assertEquals("/root\n", turnAllLineSeparatorsIntoLineFeed(new String(shellChannel.getStreamContentsAsByteArray()))); + // now try a simple command, checking for the "init" process to be found + final SshCommandChannel commandChannel = host.createRootSshChannel(Optional.empty()); + final String processToLookFor = "init"; + commandChannel.sendCommandLineSynchronously("ps axlw | grep "+processToLookFor, new ByteArrayOutputStream()); + final String output = new String(commandChannel.getStreamContentsAsByteArray()); + assertTrue(output.contains(processToLookFor)); + assertEquals(0, commandChannel.getExitStatus()); } finally { landscape.terminate(host); landscape.deleteKeyPair(region, keyName); @@ -181,31 +243,44 @@ public class ConnectivityTest { @Test public void setDNSRecordTest() { - final String hostname = "my-test-host-"+new Random().nextInt()+".wiesen-weg.de."; + final String testHostedZoneDnsName = "wiesen-weg.de"; + final String hostname = "my-test-host-"+new Random().nextInt()+"."+testHostedZoneDnsName+"."; final String ipAddress = "1.2.3.4"; + final String dnsHostedZoneId = landscape.getDNSHostedZoneId(testHostedZoneDnsName); try { - ChangeInfo changeInfo = landscape.setDNSRecordToValue(landscape.getDefaultDNSHostedZoneId(), hostname, ipAddress); + ChangeInfo changeInfo = landscape.setDNSRecordToValue(dnsHostedZoneId, hostname, ipAddress); int attempts = 10; while ((changeInfo=landscape.getUpdatedChangeInfo(changeInfo)).status() != ChangeStatus.INSYNC && --attempts > 0) { - Thread.sleep(5000); + Thread.sleep(10000); }; assertEquals(ChangeStatus.INSYNC, changeInfo.status()); } catch (Exception e) { fail(e.getMessage()); } finally { - landscape.removeDNSRecord(landscape.getDefaultDNSHostedZoneId(), hostname, ipAddress); + landscape.removeDNSRecord(dnsHostedZoneId, hostname, ipAddress); } } @Test - public void createEmptyLoadBalancerTest() { + public void createEmptyLoadBalancerTest() throws InterruptedException { final String albName = "MyAlb"+new Random().nextInt(); - final ApplicationLoadBalancer alb = landscape.createLoadBalancer(albName, region); + final ApplicationLoadBalancer alb = landscape.createLoadBalancer(albName, region); try { assertNotNull(alb); assertEquals(albName, alb.getName()); + assertTrue(Util.contains(Util.map(landscape.getLoadBalancers(region), ApplicationLoadBalancer::getArn), alb.getArn())); + // now add two rules to the load balancer and check they arrive: + final String hostnameCondition = "a.wiesen-weg.de"; + @SuppressWarnings("unchecked") + final Iterable rulesCreated = alb + .addRules(Rule.builder() + .priority("5") + .conditions(r -> r.field("host-header").hostHeaderConfig(hhc -> hhc.values(hostnameCondition))) + .actions(a -> a.type(ActionTypeEnum.FIXED_RESPONSE).fixedResponseConfig(frc -> frc.statusCode("200").messageBody("Hello world"))).build()); + assertEquals(1, Util.size(rulesCreated)); + assertTrue(hostnameCondition, rulesCreated.iterator().next().conditions().iterator().next().hostHeaderConfig().values().contains(hostnameCondition)); } finally { - landscape.deleteLoadBalancer(alb); + alb.delete(); } } @@ -220,4 +295,13 @@ public class ConnectivityTest { landscape.deleteTargetGroup(targetGroup); } } + + @Test + public void testCentralReverseProxyInEuWest2IsAvailable() throws IOException, InterruptedException, JSchException { + final ReverseProxyCluster proxy = landscape.getCentralReverseProxy(new AwsRegion("eu-west-2")); + assertEquals(1, Util.size(proxy.getHosts())); + final HttpURLConnection healthCheckConnection = (HttpURLConnection) new URL("http://"+proxy.getHosts().iterator().next().getPublicAddress().getCanonicalHostName()+proxy.getHealthCheckPath()).openConnection(); + assertEquals(200, healthCheckConnection.getResponseCode()); + healthCheckConnection.disconnect(); + } } diff --git a/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/MongoTests.java b/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/MongoTests.java new file mode 100644 index 00000000000..ee24c9ee449 --- /dev/null +++ b/java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/MongoTests.java @@ -0,0 +1,60 @@ +package com.sap.sse.landscape.aws; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.mongodb.impl.MongoProcessImpl; +import com.sap.sse.landscape.mongodb.impl.MongoProcessInReplicaSetImpl; +import com.sap.sse.landscape.mongodb.impl.MongoReplicaSetImpl; + +public class MongoTests { + private static final Optional optionalTimeout = Optional.of(Duration.ONE_MINUTE.times(5)); + + private Host localhost; + private MongoProcessImpl mongoProcess; + private MongoProcessInReplicaSetImpl mongoProcessInReplicaSet; + private MongoReplicaSetImpl mongoReplicaSet; + + @Before + public void setUp() throws UnknownHostException { + localhost = Mockito.mock(Host.class); + Mockito.when(localhost.getPublicAddress()).thenReturn(InetAddress.getByName("127.0.0.1")); + mongoProcess = new MongoProcessImpl(localhost); + mongoReplicaSet = new MongoReplicaSetImpl("rs0"); + mongoProcessInReplicaSet = new MongoProcessInReplicaSetImpl(mongoReplicaSet, 10222, localhost); + mongoReplicaSet.addReplica(mongoProcessInReplicaSet); + } + + @Test + public void testMongoReadiness() { + assertTrue(mongoProcess.isReady(optionalTimeout)); + } + + @Test + public void testMongoProcessInReplicaSetIsAvailable() { + assertTrue(mongoProcessInReplicaSet.isReady(optionalTimeout)); + } + + @Test + public void testMongoProcessInReplicaSetCanReportPriority() throws URISyntaxException { + assertTrue(mongoProcessInReplicaSet.isInReplicaSet()); + } + + @Test + public void testMd5() throws URISyntaxException { + final String hash = mongoReplicaSet.getMD5Hash("local"); + assertNotNull(hash); + assertTrue(!hash.isEmpty()); + } +} diff --git a/java/com.sap.sse.landscape.aws/META-INF/MANIFEST.MF b/java/com.sap.sse.landscape.aws/META-INF/MANIFEST.MF index e9ce37d9216..6360435d78a 100755 --- a/java/com.sap.sse.landscape.aws/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.landscape.aws/META-INF/MANIFEST.MF @@ -12,7 +12,10 @@ Require-Bundle: com.amazon.aws.aws-java-api;bundle-version="2.13.50", com.jcraft.jsch;bundle-version="0.1.54", com.sap.sse.landscape.aws.persistence, com.sap.sse.security, - com.sap.sse.mongodb + com.sap.sse.mongodb, + com.sap.sse.shared.android Import-Package: org.osgi.framework;version="1.8.0" Bundle-Activator: com.sap.sse.landscape.aws.impl.Activator -Export-Package: com.sap.sse.landscape.aws +Export-Package: com.sap.sse.landscape.aws, + com.sap.sse.landscape.aws.impl, + com.sap.sse.landscape.aws.orchestration diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java index a2724e9bb02..5fec1f2e65c 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java @@ -2,6 +2,10 @@ package com.sap.sse.landscape.aws; import com.sap.sse.common.Named; import com.sap.sse.landscape.Region; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; + +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Listener; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule; /** * Represents an AWS Application Load Balancer (ALB). When created, a default configuration with the following @@ -9,25 +13,76 @@ import com.sap.sse.landscape.Region; * and {@code access_logs.s3.prefix}, enabling {@code deletion_protection.enabled} and setting * {@code idle_timeout.timeout_seconds} to the maximum value of 4000s, furthermore spanning all availability * zones available in the region in which the ALB is deployed and using a specific security group that - * allows for HTTP and HTTPS traffic. + * allows for HTTP and HTTPS traffic.

+ * + * Furthermore, two listeners are always established: the HTTP listener forwards to a dedicated target group that + * has as its target(s) the central reverse proxy/proxies. Any HTTP request arriving there will be re-written to + * a corresponding HTTPS request and is then expected to arrive at the HTTPS listener of the same ALB.

+ * + * The HTTPS listener contains a default route that also forwards to central reverse proxy/proxies, requiring + * another ALB-specific target group for HTTPS traffic.

+ * + * The two default rules and the two listeners are not entirely exposed by this interface. Instead, clients + * will only see the non-default rule set of the HTTPS listener which is used to dynamically configure the + * landscape. * * @author Axel Uhl (D043530) * */ -public interface ApplicationLoadBalancer extends Named { +public interface ApplicationLoadBalancer extends Named { /** * The DNS name of this load balancer; can be used, e.g., to set a CNAME DNS record pointing * to this load balancer. */ String getDNSName(); - Iterable getRules(); + Iterable getRules(); - void addRules(Iterable rulesToAdd); - - void deleteRules(Iterable rulesToDelete); + void deleteRules(Rule... rulesToDelete); Region getRegion(); String getArn(); + + /** + * Application load balancer rules have a {@link Rule#priority() priority} which must be unique in the scope of a + * load balancer's listener. This way, when rules come and go, holes in the priority numbering scheme will start to exist. + * If a set of rules is to be added ({@code rulesToAdd}), consider using {@link #assignUnusedPriorities} to make room + * for interleaved or contiguous addition of the new rules. + * + * @param rulesToAdd + * rules (without an ARN set yet), specifying which rules to add to the HTTPS listener of this load + * balancer. All rules must have a priority that is not used currently by the listener. + * @return the rules created, with ARNs set + */ + Iterable addRules(Rule... rulesToAdd); + + /** + * As the rule {@link Rule#priority() priorities} within a load balancer's listener have to be unique, this method + * supports adding rules by assigning yet unused priorities to them. It keeps the order in which the {@code rules} + * are passed. If {@code forceContiguous} is {@code false}, for each rule the next available priority is chosen and + * assigned by creating a copy of the {@link Rule} object and adding it to the resulting sequence. This can lead to + * existing rules interleaving with the rules to add while ensuring that the {@code rules} have priorities in + * numerically ascending order, consistent with the order in which they were passed to this method. + *

+ * + * If {@code forceContiguous} is {@code true}, the rules that result will have contiguously increasing priority + * values, hence not interleaving with other existing rules. If there are enough contiguous unused priorities + * available, they are selected and assigned by creating copies of the {@link Rule} objects and adding them in their + * original order to the resulting sequence. Otherwise, the existing rules are "compressed" by re-numbering their + * priorities to make space for the new rules at the end of the list. + * + * @return copies of the original rules, with unused {@link Rule#priority() priorities} assigned, as passed already + * to {@link #addRules(Rule...)}. + */ + Iterable addRulesAssigningUnusedPriorities(boolean forceContiguous, Rule... rules); + + Iterable> getTargetGroups(); + + /** + * Deletes this application load balancer and all its {@link #getTargetGroups target groups}. + */ + void delete() throws InterruptedException; + + void deleteListener(Listener listener); } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsInstance.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsInstance.java index dc90eea74d6..7042424518f 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsInstance.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsInstance.java @@ -1,13 +1,5 @@ package com.sap.sse.landscape.aws; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; - -import com.jcraft.jsch.Channel; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.JSchException; import com.sap.sse.landscape.Host; import com.sap.sse.landscape.MachineImage; import com.sap.sse.landscape.application.ApplicationProcessMetrics; @@ -20,27 +12,7 @@ import com.sap.sse.landscape.application.ApplicationProcessMetrics; * */ public interface AwsInstance extends Host { - /** - * Connects to an SSH session for the username specified, using the SSH key pair used to launch the instance, and - * opens a "shell" channel. Use the {@link Channel} returned by {@link Channel#setInputStream(java.io.InputStream) - * setting an input stream} from which the commands to be sent to the server will be read, and by - * {@link Channel#setOutputStream(java.io.OutputStream) setting the output stream} to which the server will send its - * output. You will usually want to use either a {@link ByteArrayInputStream} to provide a set of predefined - * commands to sent to the server, and a {@link PipedInputStream} wrapped around a {@link PipedOutputStream} which - * you set to the channel. - */ - SshCommandChannel createSshChannel(String sshUserName) throws JSchException, IOException, InterruptedException; - - /** - * Connects to an SSH session for the "root" user with a "shell" channel - * - * @see #createSshChannel(String) - */ - SshCommandChannel createRootSshChannel() throws JSchException, IOException, InterruptedException; - - ChannelSftp createSftpChannel(String sshUserName) throws JSchException, IOException; - - ChannelSftp createRootSftpChannel() throws JSchException, IOException; - String getInstanceId(); + + void terminate(); } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java index 3fedfa9d9e2..809dd510d80 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java @@ -1,6 +1,8 @@ package com.sap.sse.landscape.aws; +import java.util.Collection; import java.util.Map; +import java.util.Optional; import com.jcraft.jsch.JSchException; import com.sap.sse.landscape.AvailabilityZone; @@ -8,14 +10,16 @@ import com.sap.sse.landscape.Host; import com.sap.sse.landscape.Landscape; import com.sap.sse.landscape.MachineImage; import com.sap.sse.landscape.Region; +import com.sap.sse.landscape.RotatingFileBasedLog; import com.sap.sse.landscape.SecurityGroup; import com.sap.sse.landscape.application.ApplicationMasterProcess; import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.application.ApplicationReplicaProcess; import com.sap.sse.landscape.aws.impl.AmazonMachineImage; import com.sap.sse.landscape.aws.impl.AwsLandscapeImpl; -import com.sap.sse.landscape.aws.impl.AwsRegion; import com.sap.sse.landscape.aws.impl.AwsTargetGroupImpl; +import com.sap.sse.landscape.mongodb.Database; +import com.sap.sse.landscape.mongodb.MongoEndpoint; import com.sap.sse.landscape.ssh.SSHKeyPair; import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; @@ -24,6 +28,11 @@ import software.amazon.awssdk.services.ec2.model.Instance; import software.amazon.awssdk.services.ec2.model.InstanceType; import software.amazon.awssdk.services.ec2.model.KeyPairInfo; import software.amazon.awssdk.services.elasticloadbalancingv2.ElasticLoadBalancingV2Client; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Listener; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancer; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancerState; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.RulePriorityPair; import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth; import software.amazon.awssdk.services.route53.Route53Client; import software.amazon.awssdk.services.route53.model.ChangeInfo; @@ -43,9 +52,11 @@ public interface AwsLandscape, ReplicaProcessT extends ApplicationReplicaProcess> extends Landscape { - static String ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME = "com.sap.sse.landscape.aws.accesskeyid"; + String ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME = "com.sap.sse.landscape.aws.accesskeyid"; - static String SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME = "com.sap.sse.landscape.aws.secretaccesskey"; + String SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME = "com.sap.sse.landscape.aws.secretaccesskey"; + + String S3_BUCKET_FOR_ALB_LOGS_SYSTEM_PROPERTY_NAME = "com.sap.sse.landscape.aws.s3bucketforalblogs"; /** * Based on system properties for the AWS access key ID and the secret access key (see @@ -63,7 +74,6 @@ extends Landscape { /** * Launches a new {@link Host} from a given image into the availability zone specified and controls network access * to that instance by setting the security groups specified for the resulting host. - * * @param keyName * the SSH key pair name to use when launching; this will grant root access with the corresponding * private key; see also {@link #getKeyPairInfo(Region, String)} @@ -72,23 +82,29 @@ extends Landscape { * concatenated, using the line separator to join them. The instance is able to read the user data throuh * the AWS SDK installed on the instance. */ - default AwsInstance launchHost(MachineImage> fromImage, InstanceType instanceType, - AwsAvailabilityZone az, String keyName, Iterable securityGroups, String... userData) { - return launchHosts(1, fromImage, instanceType, az, keyName, securityGroups, userData).iterator().next(); + default AwsInstance launchHost( + MachineImage fromImage, InstanceType instanceType, + AwsAvailabilityZone az, String keyName, Iterable securityGroups, Optional tags, String... userData) { + return launchHosts(/* numberOfHostsToLaunch */ 1, fromImage, instanceType, az, keyName, securityGroups, tags, userData).iterator().next(); } + // -------------------- technical landscape services ----------------- + /** * Launches a number of new {@link Host}s from a given image into the availability zone specified and controls * network access to that instance by setting the security groups specified for the resulting host. * @param keyName * the SSH key pair name to use when launching; this will grant root access with the corresponding * private key; see also {@link #getKeyPairInfo(Region, String)} - * @param userData TODO */ - Iterable> launchHosts(int numberOfHostsToLaunch, MachineImage> fromImage, InstanceType instanceType, - AwsAvailabilityZone az, String keyName, Iterable securityGroups, String... userData); - + Iterable> launchHosts(int numberOfHostsToLaunch, + MachineImage fromImage, InstanceType instanceType, + AwsAvailabilityZone az, String keyName, Iterable securityGroups, Optional tags, + String... userData); + AmazonMachineImage getImage(Region region, String imageId); + + AmazonMachineImage getLatestImageWithTag(Region region, String tagName, String tagValue); KeyPairInfo getKeyPairInfo(Region region, String keyName); @@ -103,7 +119,7 @@ extends Landscape { SSHKeyPair getSSHKeyPair(Region region, String keyName); - byte[] getDescryptedPrivateKey(SSHKeyPair keyPair) throws JSchException; + byte[] getDecryptedPrivateKey(SSHKeyPair keyPair) throws JSchException; void addSSHKeyPair(SSHKeyPair keyPair); @@ -126,9 +142,9 @@ extends Landscape { /** * @param hostname the fully-qualified host name */ - ChangeInfo setDNSRecordToApplicationLoadBalancer(String hostedZoneId, String hostname, ApplicationLoadBalancer alb); + ChangeInfo setDNSRecordToApplicationLoadBalancer(String hostedZoneId, String hostname, ApplicationLoadBalancer alb); - String getDefaultDNSHostedZoneId(); + String getDNSHostedZoneId(String hostedZoneName); /** * @param hostname the fully-qualified host name @@ -156,30 +172,119 @@ extends Landscape { ChangeInfo removeDNSRecord(String hostedZoneId, String hostname, String value); ChangeInfo getUpdatedChangeInfo(ChangeInfo changeInfo); - - ApplicationLoadBalancer getLoadBalancer(String loadBalancerArn, Region region); - ApplicationLoadBalancer createLoadBalancer(String name, Region region); + Iterable> getLoadBalancers(Region region); + + ApplicationLoadBalancer getLoadBalancer(String loadBalancerArn, Region region); + + /** + * Creates an application load balancer with the name and in the region specified. The method returns once the request + * has been responded to. The load balancer may still be in a pre-ready state. Use {@link #getApplicationLoadBalancerStatus(ApplicationLoadBalancer)} + * to find out more. + */ + ApplicationLoadBalancer createLoadBalancer(String name, Region region); + + Iterable getListeners(ApplicationLoadBalancer alb); + + LoadBalancerState getApplicationLoadBalancerStatus(ApplicationLoadBalancer alb); Iterable getAvailabilityZones(Region awsRegion); - AwsAvailabilityZone getAvailabilityZoneByName(AwsRegion region, String availabilityZoneName); + AwsAvailabilityZone getAvailabilityZoneByName(Region region, String availabilityZoneName); - void deleteLoadBalancer(ApplicationLoadBalancer alb); + /** + * Deletes this load balancer and all its target groups (the target groups to which this load balancer currently + * forwards any traffic). + */ + void deleteLoadBalancer(ApplicationLoadBalancer alb); + + /** + * All target groups that have the load balancer identified by the ARN as "their" load balancer which means that + * this load balancer is forwarding traffic to all those target groups. + */ + Iterable> getTargetGroupsByLoadBalancerArn(Region region, String loadBalancerArn); /** * Creates a target group with a default configuration that includes a health check URL. Stickiness is enabled with * the default duration of one day. The load balancing algorithm is set to {@code least_outstanding_requests}. + * The protocol (HTTP or HTTPS) is inferred from the port: 443 means HTTPS; anything else means HTTP. */ - TargetGroup createTargetGroup(AwsRegion region, String targetGroupName, int port, String healthCheckPath, int healthCheckPort); + TargetGroup createTargetGroup(Region region, String targetGroupName, int port, + String healthCheckPath, int healthCheckPort); - default TargetGroup getTargetGroup(AwsRegion region, String targetGroupName, String targetGroupArn) { + default TargetGroup getTargetGroup(Region region, String targetGroupName, String targetGroupArn) { return new AwsTargetGroupImpl<>(this, region, targetGroupName, targetGroupArn); } - software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroup(AwsRegion region, String targetGroupName); - + software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroup(Region region, String targetGroupName); + + software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroupByArn(Region region, String targetGroupArn); + Map, TargetHealth> getTargetHealthDescriptions(TargetGroup targetGroup); - void deleteTargetGroup(TargetGroup targetGroup); + void deleteTargetGroup(TargetGroup targetGroup); + + Iterable getLoadBalancerListenerRules(Listener loadBalancerListener, Region region); + + /** + * Use {@link Rule.Builder} to create {@link Rule} objects you'd like to set for the {@link Listener} passed as parameter. + * Obviously, there is no need to set the {@link Rule.Builder#ruleArn(String) rule's ARN} as this is created by executing + * the request. + * + * @return the rule objects, now including the {@link Rule#ruleArn() rule ARNs}, created by this request + */ + Iterable createLoadBalancerListenerRules(Region region, Listener listener, Rule... rulesToAdd); + + void deleteLoadBalancerListenerRules(Region region, Rule... rulesToDelete); + + void updateLoadBalancerListenerRulePriorities(Region region, Collection newRulePriorities); + + void deleteLoadBalancerListener(Region region, Listener listener); + + SecurityGroup getSecurityGroup(String securityGroupId, Region region); + + void addTargetsToTargetGroup( + TargetGroup targetGroup, + Iterable> targets); + + void removeTargetsFromTargetGroup( + TargetGroup targetGroup, + Iterable> targets); + + LoadBalancer getAwsLoadBalancer(String loadBalancerArn, Region region); + + // --------------- abstract landscape view -------------- + /** + * Obtains the reverse proxy in the given {@code region} that is used to receive (and possibly redirect to HTTPS or + * forward to a host proxied by the reverse proxy) all HTTP requests and any HTTPS request not handled by a + * dedicated load balancer rule, such as "cold storage" hostnames that have been archived. May return {@code null} + * in case in the given {@code region} no such reverse proxy has been configured / set up yet. + */ + ReverseProxyCluster getCentralReverseProxy(Region region); + + /** + * Each region can have a single load balancer that is the target for the "*" (asterisk, catch-all) domain. + * This load balancer shall be used for rather short-lived scope mappings and their rules because changes become + * effective immediately with the change, other than for DNS records, for example, which take a while to propagate + * through the world-wide DNS infrastructure. + */ + ApplicationLoadBalancer getNonDNSMappedLoadBalancer(Region region); + + /** + * Looks up the hostname in the DNS and assumes to get a load balancer CNAME record for it that exists in the {@code region} + * specified. The load balancer is then looked up by its {@link ApplicationLoadBalancer#getDNSName() host name}. + */ + ApplicationLoadBalancer getDNSMappedLoadBalancerFor(Region region, String hostname); + + /** + * The default MongoDB configuration to connect to. + */ + MongoEndpoint getDatabaseConfigurationForDefaultCluster(Region region); + + Database getDatabase(Region region, String databaseName); + + /** + * The region to use as the default region for instance creation, DB connectivity, reverse proxy config, ... + */ + Region getDefaultRegion(); } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ForwardToTargetGroup.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ForwardToTargetGroup.java deleted file mode 100755 index be9ad18a280..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ForwardToTargetGroup.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sap.sse.landscape.aws; - -import com.sap.sse.landscape.application.ApplicationProcessMetrics; - -public interface ForwardToTargetGroup extends LoadBalancerRuleAction { - TargetGroup getTargetGroupToForwardTo(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/HostnameCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/HostnameCondition.java deleted file mode 100755 index e00e5005f5c..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/HostnameCondition.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface HostnameCondition extends LoadBalancerRuleCondition { - Iterable getHostnames(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/HttpHeaderCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/HttpHeaderCondition.java deleted file mode 100755 index 72748e6dc66..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/HttpHeaderCondition.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sap.sse.landscape.aws; - -import java.util.Map; - -public interface HttpHeaderCondition extends LoadBalancerRuleCondition { - Map getHeaderFieldNamesAndValues(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRule.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRule.java deleted file mode 100755 index 38b6c0c6578..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRule.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sap.sse.landscape.aws; - -/** - * One or more conditions may be set; within each condition a conjunction ("OR") for more than - * one pattern or value may be possible, however limited in size (up to four or so). - * - * @author Axel Uhl (D043530) - * - */ -public interface LoadBalancerRule { - HostnameCondition getHostnameCondition(); - HttpHeaderCondition getHttpHeaderCondition(); - MethodCondition getMethodCondition(); - PathCondition getPathCondition(); - QueryStringCondition getQueryStringCondition(); - SourceIPCondition getSourceIPCondition(); - LoadBalancerRuleAction getAction(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRuleAction.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRuleAction.java deleted file mode 100755 index 068bc0cef1d..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRuleAction.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface LoadBalancerRuleAction { - -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRuleCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRuleCondition.java deleted file mode 100755 index 9509a5ebd07..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/LoadBalancerRuleCondition.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface LoadBalancerRuleCondition { - -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/MethodCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/MethodCondition.java deleted file mode 100755 index 9d920df4b64..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/MethodCondition.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface MethodCondition extends LoadBalancerRuleCondition { - Iterable getMethods(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/PathCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/PathCondition.java deleted file mode 100755 index 3c56e500e4b..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/PathCondition.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface PathCondition extends LoadBalancerRuleCondition { - Iterable getPathPatterns(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/QueryStringCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/QueryStringCondition.java deleted file mode 100755 index 4f54fe9cd0c..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/QueryStringCondition.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sap.sse.landscape.aws; - -import java.util.Map; - -public interface QueryStringCondition extends LoadBalancerRuleCondition { - Map getParameterNamesAndPatterns(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/RedirectTo.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/RedirectTo.java deleted file mode 100755 index 9d819a653bd..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/RedirectTo.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface RedirectTo extends LoadBalancerRuleAction { - enum RedirectResponse { - PERMANENTLY_MOVED(301), FOUND(302); - - private int responseCode; - - RedirectResponse(int responseCode) { - this.responseCode = responseCode; - } - - public int getResponseCode() { - return responseCode; - } - - public void setResponseCode(int responseCode) { - this.responseCode = responseCode; - } - } - - String getProtocol(); - Integer getPort(); - String getHost(); - String getPath(); - String getQuery(); - RedirectResponse getResponse(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReturnFixedResponse.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReturnFixedResponse.java deleted file mode 100755 index 00c26babb0d..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReturnFixedResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface ReturnFixedResponse extends LoadBalancerRuleAction { - int getResponseCode(); - String getContentType(); - String getResponseBody(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java index 324a678d368..5277464f01a 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java @@ -2,8 +2,8 @@ package com.sap.sse.landscape.aws; import java.util.UUID; -import com.sap.sse.common.Named; import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.Log; import com.sap.sse.landscape.application.ApplicationMasterProcess; import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.application.ApplicationReplicaProcess; @@ -33,52 +33,22 @@ import software.amazon.awssdk.services.ec2.model.InstanceType; * We think that such reverse proxies should be used as "cold-storage" handlers, mapping scopes and hostnames of content * through "default" load balancer rules. Such default rules should forward all requests to a target group that contains * the {@link #getHosts() hosts} forming this reverse proxy. By allowing for multiple hosts that all share the same - * reverse proxy configuration, scalability as well as availability can be addressed. + * reverse proxy configuration, scalability as well as availability can be addressed.

+ * + * In the future, support may be added to maintain a set of {@link TargetGroup target groups} forwarding traffic to this + * reverse proxy as hosts are added to or removed from this reverse proxy installation. * * @author Axel Uhl (D043530) * */ public interface ReverseProxy, -ReplicaProcessT extends ApplicationReplicaProcess> extends Named { - /** - * A reverse proxy may scale out by adding more hosts. - * - * @return at least one host - */ - Iterable> getHosts(); - - /** - * The target group that will be managed by this object when hosts are added and removed. This instance asserts that - * after {@link #addHost(AwsAvailabilityZone)}, {@link #addHosts(InstanceType, AwsAvailabilityZone, int)} and - * {@link #removeHost(AwsInstance)} the {@link TargetGroup#getRegisteredTargets()} will match the response of - * {@link #getHosts()}. - */ - TargetGroup getTargetGroup(); - - /** - * Adds a single host to this reverse proxy in availability zone {@code az}, using a default instance type - * {@link InstanceType#T3_SMALL}. See {@link #addHosts(InstanceType, AwsAvailabilityZone, int)} in case you'd like - * to specify a different instance type. - * - * @return the host that was added by this request; it will also be part of the response of {@link #getHosts()} now - */ - default AwsInstance addHost(AwsAvailabilityZone az) { - return addHosts(getDefaultInstanceType(), az, /* numberOfHostsToAdd */ 1).iterator().next(); - } - +ReplicaProcessT extends ApplicationReplicaProcess, +LogT extends Log> { default InstanceType getDefaultInstanceType() { return InstanceType.T3_SMALL; } - /** - * Add zero or more hosts of the instance type specified to the availability zone {@code az}. - * - * @return the hosts that were added by this request; they will also be part of the response of {@link #getHosts()} - * now - */ - Iterable> addHosts(InstanceType instanceType, AwsAvailabilityZone az, int numberOfHostsToAdd); - /** * Configures a redirect in this reverse proxy such that requests for it will go to the * {@code /index.html} landing page for the application replica set provided. @@ -121,17 +91,12 @@ ReplicaProcessT extends ApplicationReplicaProcess host); - + /** * {@link AwsLandscape#terminate(AwsInstance) Terminates} all {@link #getHosts() hosts} that form this reverse * proxy. */ void terminate(); + + String getHealthCheckPath(); } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxyCluster.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxyCluster.java new file mode 100644 index 00000000000..d68f7e91299 --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxyCluster.java @@ -0,0 +1,47 @@ +package com.sap.sse.landscape.aws; + +import com.sap.sse.landscape.Log; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; + +import software.amazon.awssdk.services.ec2.model.InstanceType; + +/** + * A cluster of {@link ReverseProxy} processes, for scale and/or availability. Can be grown or shrunk by adding or + * removing {@link ReverseProxy} instances, respectively. All the {@link ReverseProxy} processes share the same + * configuration. Updating it through this interface ensures the configuration is updated in all participating + * {@link ReverseProxy} processes, with the proxies reloaded to reflect the changes. + * + * @author Axel Uhl (D043530) + * + */ +public interface ReverseProxyCluster, +ReplicaProcessT extends ApplicationReplicaProcess, +LogT extends Log> + extends ReverseProxy { + /** + * A reverse proxy may scale out by adding more hosts. + * + * @return at least one host + */ + Iterable> getHosts(); + + /** + * Add one host of the instance type specified to the availability zone {@code az}. + * + * @return the hosts that were added by this request; they will also be part of the response of {@link #getHosts()} + * now + */ + AwsInstance createHost(InstanceType instanceType, AwsAvailabilityZone az, String keyName); + + void addHost(AwsInstance host); + + /** + * Removes a single host from this reverse proxy, terminating the host. When trying to remove the last remaining + * host, an {@link IllegalStateException} will be thrown and the method will not complete the request. Consider + * using {@link #terminate()} to terminate all hosts forming this reverse proxy. + */ + void removeHost(AwsInstance host); +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/SourceIPCondition.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/SourceIPCondition.java deleted file mode 100755 index 8b2d57b9921..00000000000 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/SourceIPCondition.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.landscape.aws; - -public interface SourceIPCondition extends LoadBalancerRuleCondition { - Iterable getCidrs(); -} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/Tags.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/Tags.java new file mode 100644 index 00000000000..5f7d4ccb14e --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/Tags.java @@ -0,0 +1,17 @@ +package com.sap.sse.landscape.aws; + +import java.util.Map.Entry; + +import com.sap.sse.landscape.aws.impl.TagsImpl; + +public interface Tags extends Iterable>{ + static Tags with(String key, String value) { + return new TagsImpl(key, value); + } + + Tags and(String key, String value); + + static Tags empty() { + return new TagsImpl(); + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/TargetGroup.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/TargetGroup.java index 111b0279c27..c16d8a961eb 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/TargetGroup.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/TargetGroup.java @@ -4,8 +4,8 @@ import java.util.Collections; import java.util.Map; import com.sap.sse.common.Named; +import com.sap.sse.landscape.Region; import com.sap.sse.landscape.application.ApplicationProcessMetrics; -import com.sap.sse.landscape.aws.impl.AwsRegion; import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth; @@ -16,7 +16,7 @@ import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth * */ public interface TargetGroup extends Named { - AwsRegion getRegion(); + Region getRegion(); Map, TargetHealth> getRegisteredTargets(); @@ -33,4 +33,6 @@ public interface TargetGroup> targets); String getTargetGroupArn(); + + ApplicationLoadBalancer getLoadBalancer(); } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AbstractApacheReverseProxy.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AbstractApacheReverseProxy.java new file mode 100644 index 00000000000..f3f7170c344 --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AbstractApacheReverseProxy.java @@ -0,0 +1,28 @@ +package com.sap.sse.landscape.aws.impl; + +import com.sap.sse.landscape.RotatingFileBasedLog; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.ReverseProxy; + +public abstract class AbstractApacheReverseProxy, +ReplicaProcessT extends ApplicationReplicaProcess> +implements ReverseProxy { + private final AwsLandscape landscape; + + public AbstractApacheReverseProxy(AwsLandscape landscape) { + this.landscape = landscape; + } + + protected AwsLandscape getLandscape() { + return landscape; + } + + @Override + public String getHealthCheckPath() { + return "/internal-server-status"; + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/Activator.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/Activator.java index 0a5eb1a46e2..76b34b18f88 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/Activator.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/Activator.java @@ -18,7 +18,8 @@ public class Activator implements BundleActivator { landscape = new AwsLandscapeImpl<>(System.getProperty(AwsLandscape.ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME), System.getProperty(AwsLandscape.SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME), PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(), - PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory()); + PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(), + System.getProperty(AwsLandscape.S3_BUCKET_FOR_ALB_LOGS_SYSTEM_PROPERTY_NAME)); } public static Activator getInstance() { diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AmazonMachineImage.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AmazonMachineImage.java index 70c4109a49c..d307373e47e 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AmazonMachineImage.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AmazonMachineImage.java @@ -10,11 +10,10 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.landscape.MachineImage; import com.sap.sse.landscape.Region; import com.sap.sse.landscape.application.ApplicationProcessMetrics; -import com.sap.sse.landscape.aws.AwsInstance; import software.amazon.awssdk.services.ec2.model.Image; -public class AmazonMachineImage implements MachineImage> { +public class AmazonMachineImage implements MachineImage { private final Image image; private final Region region; @@ -48,6 +47,7 @@ public class AmazonMachineImage updateAllPackages() { + // launch with "image-upgrade" as the only user data, then produce the new image // TODO Implement MachineImage.updateAllPackages(...) return null; } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java index 6c2bfc8b5b4..a0cd3c111c7 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java @@ -1,13 +1,11 @@ package com.sap.sse.landscape.aws.impl; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; +import java.util.Optional; import java.util.UUID; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.NamedImpl; +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.RotatingFileBasedLog; import com.sap.sse.landscape.application.ApplicationMasterProcess; import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.application.ApplicationReplicaProcess; @@ -16,10 +14,6 @@ import com.sap.sse.landscape.application.Scope; import com.sap.sse.landscape.aws.AwsAvailabilityZone; import com.sap.sse.landscape.aws.AwsInstance; import com.sap.sse.landscape.aws.AwsLandscape; -import com.sap.sse.landscape.aws.ReverseProxy; -import com.sap.sse.landscape.aws.TargetGroup; - -import software.amazon.awssdk.services.ec2.model.InstanceType; /** * An Apache2-based reverse proxy implementation (httpd) that makes specific assumptions about the availability of an @@ -34,30 +28,13 @@ import software.amazon.awssdk.services.ec2.model.InstanceType; public class ApacheReverseProxy, ReplicaProcessT extends ApplicationReplicaProcess> -extends NamedImpl implements ReverseProxy { - private static final long serialVersionUID = 8019146973512856147L; - private final AwsLandscape landscape; - private final AwsRegion region; - private final String targetGroupArn; +extends AbstractApacheReverseProxy +implements com.sap.sse.landscape.Process { + private AwsInstance host; - public ApacheReverseProxy(String name, AwsLandscape landscape, AwsRegion region, Map numberOfInstancesPerAz) { - super(name); - this.landscape = landscape; - this.region = region; - final TargetGroup targetGroup = landscape.createTargetGroup(region, getTargetGroupName(), /* port */ 80, - getHealthCheckUrl(), /* health check port */ 80); - targetGroupArn = targetGroup.getTargetGroupArn(); - for (final Entry e : numberOfInstancesPerAz.entrySet()) { - targetGroup.addTargets(addHosts(getDefaultInstanceType(), e.getKey(), e.getValue())); - } - } - - private String getHealthCheckUrl() { - return "/internal-server-status"; - } - - private String getTargetGroupName() { - return "ReverseProxy-"+getName(); + public ApacheReverseProxy(AwsLandscape landscape, AwsInstance host) { + super(landscape); + this.host = host; } @Override @@ -102,39 +79,40 @@ extends NamedImpl implements ReverseProxy> getHosts() { - // TODO Implement ReverseProxy.getHosts(...) - return null; - } - - @Override - public Iterable> addHosts(InstanceType instanceType, AwsAvailabilityZone az, int numberOfHostsToAdd) { - // TODO Implement ReverseProxy.addHost(...) - return null; - } - - @Override - public void removeHost(AwsInstance host) { - assert Util.contains(getHosts(), host); - if (Util.size(getHosts()) == 1) { - throw new IllegalStateException("Trying to remove the last hosts of reverse proxy "+this+". Use terminate() instead"); - } - landscape.terminate(host); - } - @Override public void terminate() { - Set> hosts = new HashSet<>(); - Util.addAll(getHosts(), hosts); - for (final AwsInstance host : hosts) { - landscape.terminate(host); - } - landscape.deleteTargetGroup(getTargetGroup()); + getLandscape().terminate(host); } @Override - public TargetGroup getTargetGroup() { - return landscape.getTargetGroup(region, getTargetGroupName(), targetGroupArn); + public int getPort() { + return 443; // TODO currently, we offload SSL only at the reverse proxies; but we should change this to SSL offloading at the load balancer, and then this would have to become 80 (HTTP) + } + + /** + * Making things more specific: as we're in the AWS universe here, the {@link Host} returned more specifically is an + * {@link AwsInstance}. + */ + @Override + public AwsInstance getHost() { + return host; + } + + @Override + public RotatingFileBasedLog getLog() { + // TODO Implement Process.getLog(...) + return null; + } + + @Override + public MetricsT getMetrics() { + // TODO Implement Process.getMetrics(...) + return null; + } + + @Override + public boolean isReady(Optional optionalTimeout) { + // TODO Implement Process.isReady(...) + return false; } } \ No newline at end of file diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java new file mode 100644 index 00000000000..ed26d643825 --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java @@ -0,0 +1,123 @@ +package com.sap.sse.landscape.aws.impl; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import com.sap.sse.common.Util; +import com.sap.sse.landscape.Log; +import com.sap.sse.landscape.MachineImage; +import com.sap.sse.landscape.Region; +import com.sap.sse.landscape.RotatingFileBasedLog; +import com.sap.sse.landscape.SecurityGroup; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.application.ApplicationReplicaSet; +import com.sap.sse.landscape.application.Scope; +import com.sap.sse.landscape.aws.AwsAvailabilityZone; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.ReverseProxyCluster; +import com.sap.sse.landscape.aws.Tags; + +import software.amazon.awssdk.services.ec2.model.InstanceType; + +public class ApacheReverseProxyCluster, +ReplicaProcessT extends ApplicationReplicaProcess, +LogT extends Log> + extends AbstractApacheReverseProxy +implements ReverseProxyCluster { + private Set> hosts; + + public ApacheReverseProxyCluster(AwsLandscape landscape) { + super(landscape); + this.hosts = new HashSet<>(); + } + + @Override + public Iterable> getHosts() { + return Collections.unmodifiableCollection(hosts); + } + + private Iterable> getReverseProxies() { + return Util.map(hosts, host->new ApacheReverseProxy<>(getLandscape(), host)); + } + + @Override + public void addHost(AwsInstance host) { + hosts.add(host); + } + + @Override + public AwsInstance createHost(InstanceType instanceType, AwsAvailabilityZone az, String keyName) { + return getLandscape().launchHost(getAmiId(), instanceType, az, keyName, Collections.singleton(getSecurityGroup(az.getRegion())), Optional.of(Tags.with("Name", "ReverseProxy"))); + } + + @Override + public void removeHost(AwsInstance host) { + assert Util.contains(getHosts(), host); + if (Util.size(getHosts()) == 1) { + throw new IllegalStateException("Trying to remove the last hosts of reverse proxy "+this+". Use terminate() instead"); + } + getLandscape().terminate(host); // this assumes that the host is running only the reverse proxy process... + } + + private SecurityGroup getSecurityGroup(Region region) { + return getLandscape().getDefaultSecurityGroupForCentralReverseProxy(region); + } + + private MachineImage getAmiId() { + // TODO Implement ApacheReverseProxy.getAmiId(...) + return null; + } + + @Override + public void terminate() { + Set> hosts = new HashSet<>(); + Util.addAll(getHosts(), hosts); + for (final AwsInstance host : hosts) { + getLandscape().terminate(host); + } + } + + @Override + public void setPlainRedirect(String hostname, + ApplicationReplicaSet applicationReplicaSet) { + getReverseProxies().forEach(proxy->proxy.setPlainRedirect(hostname, applicationReplicaSet)); + } + + @Override + public void setHomeRedirect(String hostname, + ApplicationReplicaSet applicationReplicaSet) { + getReverseProxies().forEach(proxy->proxy.setHomeRedirect(hostname, applicationReplicaSet)); + } + + @Override + public void setEventRedirect(String hostname, + ApplicationReplicaSet applicationReplicaSet, + UUID eventId) { + getReverseProxies().forEach(proxy->proxy.setEventRedirect(hostname, applicationReplicaSet, eventId)); + } + + @Override + public void setEventSeriesRedirect(String hostname, + ApplicationReplicaSet applicationReplicaSet, + UUID leaderboardGroupId) { + getReverseProxies().forEach(proxy->proxy.setEventSeriesRedirect(hostname, applicationReplicaSet, leaderboardGroupId)); + } + + @Override + public void setScopeRedirect(Scope scope, + ApplicationReplicaSet applicationReplicaSet) { + getReverseProxies().forEach(proxy->proxy.setScopeRedirect(scope, applicationReplicaSet)); + } + + @Override + public void removeRedirect(String hostname) { + getReverseProxies().forEach(proxy->proxy.removeRedirect(hostname)); + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java index d37ccdd2850..4d660b05a76 100644 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java @@ -1,20 +1,44 @@ package com.sap.sse.landscape.aws.impl; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.sap.sse.common.Duration; +import com.sap.sse.common.Util; +import com.sap.sse.landscape.Region; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.aws.ApplicationLoadBalancer; -import com.sap.sse.landscape.aws.LoadBalancerRule; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.TargetGroup; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Listener; import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancer; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.ProtocolEnum; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.RulePriorityPair; -public class ApplicationLoadBalancerImpl implements ApplicationLoadBalancer { +public class ApplicationLoadBalancerImpl +implements ApplicationLoadBalancer { private static final long serialVersionUID = -5297220031399131769L; - final LoadBalancer loadBalancer; - - private final com.sap.sse.landscape.Region region; + /** + * The maximum {@link Rule#priority()} that can be used within a listener + */ + private static final int MAX_PRIORITY = 50000; - public ApplicationLoadBalancerImpl(com.sap.sse.landscape.Region region, LoadBalancer loadBalancer) { + private final LoadBalancer loadBalancer; + + private final Region region; + + private final AwsLandscape landscape; + + public ApplicationLoadBalancerImpl(Region region, LoadBalancer loadBalancer, AwsLandscape landscape) { this.region = region; this.loadBalancer = loadBalancer; + this.landscape = landscape; } @Override @@ -33,26 +57,134 @@ public class ApplicationLoadBalancerImpl implements ApplicationLoadBalancer { } @Override - public com.sap.sse.landscape.Region getRegion() { + public Region getRegion() { return region; } @Override - public Iterable getRules() { - // TODO Auto-generated method stub - return null; + public Iterable getRules() { + final Listener httpsListener = getListener(ProtocolEnum.HTTPS); + return landscape.getLoadBalancerListenerRules(httpsListener, getRegion()); + } + + @Override + public void deleteListener(Listener listener) { + landscape.deleteLoadBalancerListener(getRegion(), listener); + } + + private Listener getListener(ProtocolEnum protocol) { + return Util.filter(landscape.getListeners(this), l->l.protocol() == protocol).iterator().next(); } @Override - public void addRules(Iterable rulesToAdd) { - // TODO Auto-generated method stub - + public Iterable addRules(Rule... rulesToAdd) { + return landscape.createLoadBalancerListenerRules(region, getListener(ProtocolEnum.HTTPS), rulesToAdd); } @Override - public void deleteRules(Iterable rulesToDelete) { - // TODO Auto-generated method stub - + public Iterable addRulesAssigningUnusedPriorities(boolean forceContiguous, Rule... rules) { + final Iterable existingRules = getRules(); + if (Util.size(existingRules)-1 + rules.length > MAX_PRIORITY) { // -1 due to the default rule being part of existingRules + throw new IllegalArgumentException("The "+rules.length+" new rules won't find enough unused priority numbers because there are already "+ + (Util.size(existingRules)-1)+" of them and together they would exceed the maximum of "+MAX_PRIORITY+" by "+ + (Util.size(existingRules)-1 + rules.length - MAX_PRIORITY)); + } + final List result = new ArrayList<>(rules.length); + final List sortedExistingNonDefaultRules = new ArrayList<>(Util.size(existingRules)-1); + Util.addAll(Util.filter(existingRules, r->!r.isDefault()), sortedExistingNonDefaultRules); + Collections.sort(sortedExistingNonDefaultRules, (r1, r2)->Integer.valueOf(r1.priority()).compareTo(Integer.valueOf(r2.priority()))); + final int stepwidth; + if (forceContiguous) { + stepwidth = rules.length; + } else { + stepwidth = 1; + } + int rulesIndex = 0; + int previousPriority = 0; + final Iterator existingRulesIter = sortedExistingNonDefaultRules.iterator(); + while (rulesIndex < rules.length) { + // find next available slot + int nextPriority = MAX_PRIORITY+1; // if no further rule exists, the usable gap ends after MAX_PRIORITY + while (existingRulesIter.hasNext() && (nextPriority=Integer.valueOf(existingRulesIter.next().priority())) <= previousPriority+stepwidth) { + // not enough space for stepwidth many rules; keep on searching + previousPriority = nextPriority; + if (!existingRulesIter.hasNext()) { + nextPriority = MAX_PRIORITY+1; + } + } + if (previousPriority+stepwidth > MAX_PRIORITY) { + if (forceContiguous) { + previousPriority = squeezeExistingRulesAndReturnLastUsedPriority(sortedExistingNonDefaultRules); + nextPriority = MAX_PRIORITY+1; + // we previously checked already that there is enough room for the new set of rules + assert previousPriority + rules.length <= MAX_PRIORITY; + } else { + throw new IllegalStateException( + "The " + rules.length + " new rules don't fit into the existing rule set of load balancer " + + getName() + " without exceeding the maximum priority of " + MAX_PRIORITY); + } + } + while (rulesIndex < rules.length && ++previousPriority < nextPriority) { + final int priorityToUseForNextRule = previousPriority; + result.add(rules[rulesIndex++].copy(b->b.priority(""+priorityToUseForNextRule))); + } + } + addRules(result.toArray(new Rule[0])); + return result; } + private int squeezeExistingRulesAndReturnLastUsedPriority(final List sortedExistingNonDefaultRules) { + final List newPrioritiesForExistingRules = new LinkedList<>(); + int priority = 0; + for (final Rule existingRule : sortedExistingNonDefaultRules) { + newPrioritiesForExistingRules.add(RulePriorityPair.builder().ruleArn(existingRule.ruleArn()).priority(++priority).build()); + } + landscape.updateLoadBalancerListenerRulePriorities(getRegion(), newPrioritiesForExistingRules); + return priority; + } + + @Override + public void deleteRules(Rule... rulesToDelete) { + landscape.deleteLoadBalancerListenerRules(region, rulesToDelete); + } + + @Override + public Iterable> getTargetGroups() { + return landscape.getTargetGroupsByLoadBalancerArn(getRegion(), getArn()); + } + + private void deleteAllRules() { + for (final Rule rule : getRules()) { + if (!rule.isDefault()) { + deleteRules(rule); + } + } + } + + @Override + public void delete() throws InterruptedException { + // first obtain the target groups to which, based on the current ALB configuration, traffic is forwarded + final Iterable> targetGroups = getTargetGroups(); + // now delete the rules to free up all target groups to which the ALB could have forwarded, except the default rule + deleteAllRules(); + deleteAllListeners(); + landscape.deleteLoadBalancer(this); + Thread.sleep(Duration.ONE_SECOND.times(5).asMillis()); // wait a bit until the target groups are no longer considered "in use" + // now that all target groups the ALB used are freed up, delete them: + for (final TargetGroup targetGroup : targetGroups) { + landscape.deleteTargetGroup(landscape.getTargetGroup(getRegion(), targetGroup.getName(), targetGroup.getTargetGroupArn())); + } + } + + private void deleteListener(ProtocolEnum protocol) { + final Listener httpListener = getListener(protocol); + if (httpListener != null) { + deleteListener(httpListener); + } + } + + private void deleteAllListeners() { + deleteListener(ProtocolEnum.HTTP); + deleteListener(ProtocolEnum.HTTPS); + } } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java index 03878c73b96..6a30fa09d4f 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java @@ -6,6 +6,7 @@ import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.Optional; import java.util.logging.Logger; import com.jcraft.jsch.Channel; @@ -16,7 +17,7 @@ import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; -import com.sap.sse.landscape.AvailabilityZone; +import com.sap.sse.common.Util; import com.sap.sse.landscape.Log; import com.sap.sse.landscape.Metrics; import com.sap.sse.landscape.Process; @@ -25,9 +26,10 @@ import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.aws.AwsAvailabilityZone; import com.sap.sse.landscape.aws.AwsInstance; import com.sap.sse.landscape.aws.AwsLandscape; -import com.sap.sse.landscape.aws.SshCommandChannel; import com.sap.sse.landscape.ssh.JCraftLogAdapter; import com.sap.sse.landscape.ssh.SSHKeyPair; +import com.sap.sse.landscape.ssh.SshCommandChannel; +import com.sap.sse.landscape.ssh.SshCommandChannelImpl; import com.sap.sse.landscape.ssh.YesUserInfo; import software.amazon.awssdk.services.ec2.model.Instance; @@ -37,9 +39,9 @@ public class AwsInstanceImpl landscape; + private final AwsLandscape landscape; - public AwsInstanceImpl(String instanceId, AwsAvailabilityZone availabilityZone, AwsLandscape landscape) { + public AwsInstanceImpl(String instanceId, AwsAvailabilityZone availabilityZone, AwsLandscape landscape) { this.instanceId = instanceId; this.availabilityZone = availabilityZone; this.landscape = landscape; @@ -49,7 +51,7 @@ public class AwsInstanceImpl timeoutNullMeaningForever) { InetAddress result; final TimePoint started = TimePoint.now(); while ((result = getPublicAddress()) == null && - (timeoutNullMeaningForever == null || - started.until(TimePoint.now()).compareTo(timeoutNullMeaningForever) < 0)); + (!timeoutNullMeaningForever.isPresent() || + started.until(TimePoint.now()).compareTo(timeoutNullMeaningForever.get()) < 0)); return result; } @@ -91,7 +93,7 @@ public class AwsInstanceImpl optionalTimeout) throws JSchException, IOException, InterruptedException { + return createSshChannel(ROOT_USER_NAME, optionalTimeout); } /** @@ -118,28 +120,43 @@ public class AwsInstanceImpl optionalTimeout) throws JSchException, IOException, InterruptedException { + return new SshCommandChannelImpl((ChannelExec) createSshChannelInternal(sshUserName, "exec", optionalTimeout)); } - private Channel createSshChannelInternal(String sshUserName, String channelType) throws JSchException, IOException { + private Channel createSshChannelInternal(String sshUserName, String channelType, Optional optionalTimeout) throws JSchException, IOException { + final TimePoint start = TimePoint.now(); + final int connectTimeoutInMillis = 5000; logger.info("Creating SSH "+channelType+" channel for SSH user "+sshUserName+ " to instance with ID "+getInstanceId()); - final Session session = createSshSession(sshUserName); - session.setUserInfo(new YesUserInfo()); - session.connect(/* timeout in millis */ 5000); - final Channel channel = session.openChannel(channelType); + Channel channel = null; + while (channel == null && (!optionalTimeout.isPresent() || start.until(TimePoint.now()).compareTo(optionalTimeout.get()) < 0)) { + try { + final Session session = createSshSession(sshUserName); + session.setUserInfo(new YesUserInfo()); + session.connect(/* timeout in millis */ connectTimeoutInMillis); + channel = session.openChannel(channelType); + } catch (JSchException | IllegalStateException e) { + logger.info(e.getMessage() + + " while trying to connect. Probably timeout trying early SSH connection."); + } + if (optionalTimeout.isPresent()) { + logger.info("Retrying until "+start.plus(optionalTimeout.get())); + } else { + logger.info("Retrying forever"); + } + } return channel; } @Override - public ChannelSftp createSftpChannel(String sshUserName) throws JSchException, IOException { - return (ChannelSftp) createSshChannelInternal(sshUserName, "sftp"); + public ChannelSftp createSftpChannel(String sshUserName, Optional optionalTimeout) throws JSchException, IOException { + return (ChannelSftp) createSshChannelInternal(sshUserName, "sftp", optionalTimeout); } @Override - public ChannelSftp createRootSftpChannel() throws JSchException, IOException { - return createSftpChannel(ROOT_USER_NAME); + public ChannelSftp createRootSftpChannel(Optional optionalTimeout) throws JSchException, IOException { + return createSftpChannel(ROOT_USER_NAME, optionalTimeout); } @Override @@ -149,45 +166,23 @@ public class AwsInstanceImpl getSecurityGroups() { - // TODO Implement AwsInstance.getSecurityGroups(...) - return null; + return Util.map(getInstance().securityGroups(), groupIdentifier -> + landscape.getSecurityGroup(groupIdentifier.groupId(), getRegion())); } @Override public String getInstanceId() { return instanceId; } + + @Override + public void terminate() { + landscape.terminate(this); + } } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java index 7d7a80777b4..d9f7f46f36b 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java @@ -2,11 +2,17 @@ package com.sap.sse.landscape.aws.impl; import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; import java.util.logging.Logger; import com.jcraft.jsch.JSch; @@ -17,6 +23,7 @@ import com.sap.sse.common.Util.Pair; import com.sap.sse.landscape.AvailabilityZone; import com.sap.sse.landscape.Host; import com.sap.sse.landscape.MachineImage; +import com.sap.sse.landscape.RotatingFileBasedLog; import com.sap.sse.landscape.SecurityGroup; import com.sap.sse.landscape.application.ApplicationMasterProcess; import com.sap.sse.landscape.application.ApplicationProcessMetrics; @@ -27,10 +34,16 @@ import com.sap.sse.landscape.aws.ApplicationLoadBalancer; import com.sap.sse.landscape.aws.AwsAvailabilityZone; import com.sap.sse.landscape.aws.AwsInstance; import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.ReverseProxyCluster; +import com.sap.sse.landscape.aws.Tags; import com.sap.sse.landscape.aws.TargetGroup; import com.sap.sse.landscape.aws.persistence.DomainObjectFactory; import com.sap.sse.landscape.aws.persistence.MongoObjectFactory; import com.sap.sse.landscape.aws.persistence.PersistenceFactory; +import com.sap.sse.landscape.mongodb.Database; +import com.sap.sse.landscape.mongodb.MongoEndpoint; +import com.sap.sse.landscape.mongodb.impl.DatabaseImpl; +import com.sap.sse.landscape.rabbitmq.RabbitMQEndpoint; import com.sap.sse.landscape.ssh.SSHKeyPair; import com.sap.sse.mongodb.MongoDBService; import com.sap.sse.security.SessionUtils; @@ -49,30 +62,48 @@ import software.amazon.awssdk.services.ec2.model.DescribeImagesRequest; import software.amazon.awssdk.services.ec2.model.DescribeImagesResponse; import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest; import software.amazon.awssdk.services.ec2.model.DescribeKeyPairsRequest; +import software.amazon.awssdk.services.ec2.model.Filter; +import software.amazon.awssdk.services.ec2.model.Image; import software.amazon.awssdk.services.ec2.model.ImportKeyPairRequest; import software.amazon.awssdk.services.ec2.model.Instance; import software.amazon.awssdk.services.ec2.model.InstanceType; import software.amazon.awssdk.services.ec2.model.KeyPairInfo; import software.amazon.awssdk.services.ec2.model.Placement; +import software.amazon.awssdk.services.ec2.model.ResourceType; import software.amazon.awssdk.services.ec2.model.RunInstancesRequest; +import software.amazon.awssdk.services.ec2.model.RunInstancesRequest.Builder; import software.amazon.awssdk.services.ec2.model.RunInstancesResponse; import software.amazon.awssdk.services.ec2.model.Subnet; +import software.amazon.awssdk.services.ec2.model.Tag; +import software.amazon.awssdk.services.ec2.model.TagSpecification; import software.amazon.awssdk.services.ec2.model.TerminateInstancesRequest; import software.amazon.awssdk.services.ec2.model.Vpc; import software.amazon.awssdk.services.elasticloadbalancingv2.ElasticLoadBalancingV2Client; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Action; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.ActionTypeEnum; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Certificate; import software.amazon.awssdk.services.elasticloadbalancingv2.model.CreateLoadBalancerRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.CreateLoadBalancerResponse; import software.amazon.awssdk.services.elasticloadbalancingv2.model.CreateTargetGroupRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.DeleteLoadBalancerRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.DeleteTargetGroupRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.DescribeLoadBalancersRequest; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.DescribeLoadBalancersResponse; import software.amazon.awssdk.services.elasticloadbalancingv2.model.DescribeTargetGroupsRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.DescribeTargetHealthRequest; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Listener; import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancer; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancerAttribute; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancerState; import software.amazon.awssdk.services.elasticloadbalancingv2.model.ModifyTargetGroupAttributesRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.ProtocolEnum; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.RulePriorityPair; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.SetRulePrioritiesRequest; import software.amazon.awssdk.services.elasticloadbalancingv2.model.SubnetMapping; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetDescription; import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroupAttribute; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroupTuple; import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth; import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetTypeEnum; import software.amazon.awssdk.services.route53.Route53Client; @@ -92,15 +123,23 @@ MasterProcessT extends ApplicationMasterProcess> implements AwsLandscape { private static final Logger logger = Logger.getLogger(AwsLandscapeImpl.class.getName()); private static final long DEFAULT_DNS_TTL_MILLIS = 60000l; + // TODO the sapsailing.com certificate's ARN where the certificate is valid until 2021-05-07; we need a certifiate per region + private static final String DEFAULT_CERTIFICATE_ARN = "arn:aws:acm:eu-west-2:017363970217:certificate/48c51d6a-b4f2-4fa0-8dcc-426cd9c7aadc"; + // TODO the "Java Application with Reverse Proxy" security group in eu-west-2 for experimenting; we need this security group per region + private static final String DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_1 = "sg-eaf31e85"; + private static final String DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_2 = "sg-0b2afd48960251280"; + private static final String DEFAULT_NON_DNS_MAPPED_ALB_NAME = "DefaultDynamicALB"; private final String accessKeyId; private final String secretAccessKey; private final MongoObjectFactory mongoObjectFactory; private ConcurrentMap, SSHKeyPair> sshKeyPairs; private final AwsRegion globalRegion; + private final Map> centralReverseProxyByRegion; + private final String s3BucketForAlbLogs; /** * Used for the symmetric encryption / decryption of private SSH keys. See also - * {@link #getDescryptedPrivateKey(SSHKeyPair)}. + * {@link #getDecryptedPrivateKey(SSHKeyPair)}. */ private final byte[] privateKeyEncryptionPassphrase; @@ -108,20 +147,29 @@ ReplicaProcessT extends ApplicationReplicaProcess(); this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; this.globalRegion = new AwsRegion(Region.AWS_GLOBAL); this.mongoObjectFactory = mongoObjectFactory; this.sshKeyPairs = new ConcurrentHashMap, SSHKeyPair>(); + this.s3BucketForAlbLogs = s3BucketForAlbLogs; for (final SSHKeyPair keyPair : domainObjectFactory.loadSSHKeyPairs()) { internalAddKeyPair(keyPair); } + // TODO automate handling of central reverse proxy instances across regions and AZs, e.g., based on tags + final AwsRegion euWest2 = new AwsRegion("eu-west-2"); + final AwsInstanceImpl euWest2CentralReverseProxyInstance = new AwsInstanceImpl("i-0cb21cef39d853b34", getAvailabilityZoneByName(euWest2, "eu-west-2a"), this); + final ApacheReverseProxyCluster euWest2CentralReverseProxy = new ApacheReverseProxyCluster<>( + this); + euWest2CentralReverseProxy.addHost(euWest2CentralReverseProxyInstance); + centralReverseProxyByRegion.put(euWest2, euWest2CentralReverseProxy); } /** @@ -169,7 +217,7 @@ ReplicaProcessT extends ApplicationReplicaProcess createLoadBalancer(String name, com.sap.sse.landscape.Region region) { Region awsRegion = getRegion(region); final ElasticLoadBalancingV2Client client = getLoadBalancingClient(awsRegion); final Iterable availabilityZones = getAvailabilityZones(region); @@ -178,12 +226,100 @@ ReplicaProcessT extends ApplicationReplicaProcessb.loadBalancerArn(response.loadBalancers().iterator().next().loadBalancerArn()). + attributes( + LoadBalancerAttribute.builder().key("access_logs.s3.enabled").value("true").build(), + LoadBalancerAttribute.builder().key("access_logs.s3.bucket").value(s3BucketForAlbLogs).build(), + LoadBalancerAttribute.builder().key("idle_timeout.timeout_seconds").value("4000").build()).build()); + final ApplicationLoadBalancer result = new ApplicationLoadBalancerImpl<>(region, response.loadBalancers().iterator().next(), this); + createLoadBalancerListener(result, ProtocolEnum.HTTP); + createLoadBalancerListener(result, ProtocolEnum.HTTPS); + return result; + } + + private Listener createLoadBalancerListener(ApplicationLoadBalancer alb, ProtocolEnum protocol) { + final int port = protocol==ProtocolEnum.HTTP?80:443; + final ReverseProxyCluster reverseProxy = getCentralReverseProxy(alb.getRegion()); + final TargetGroup defaultTargetGroup = createTargetGroup(alb.getRegion(), "DefTG-"+alb.getName()+"-"+protocol.name(), + port, reverseProxy.getHealthCheckPath(), /* healthCheckPort */ port); + defaultTargetGroup.addTargets(reverseProxy.getHosts()); + return getLoadBalancingClient( + getRegion(alb.getRegion())) + .createListener(l -> { + l.loadBalancerArn(alb.getArn()).protocol(protocol) + .port(port) + .defaultActions(Action.builder() + .targetGroupArn(defaultTargetGroup.getTargetGroupArn()) + .type(ActionTypeEnum.FORWARD) + .forwardConfig(f -> f.targetGroups(TargetGroupTuple.builder() + .targetGroupArn(defaultTargetGroup.getTargetGroupArn()).build()) + .build()) + .build()); + if (protocol==ProtocolEnum.HTTPS) { + l.certificates(Certificate.builder().certificateArn(DEFAULT_CERTIFICATE_ARN).build()); + } + }) + .listeners().iterator().next(); + } + + @Override + public void deleteLoadBalancer(ApplicationLoadBalancer alb) { + getLoadBalancingClient(getRegion(alb.getRegion())).deleteLoadBalancer(DeleteLoadBalancerRequest.builder().loadBalancerArn(alb.getArn()).build()); + } + + @Override + public Iterable> getTargetGroupsByLoadBalancerArn(com.sap.sse.landscape.Region region, String loadBalancerArn) { + return Util.map(getLoadBalancingClient(getRegion(region)).describeTargetGroups(tg->tg.loadBalancerArn(loadBalancerArn)).targetGroups(), + tg->new AwsTargetGroupImpl<>(this, region, tg.targetGroupName(), tg.targetGroupArn())); + } + + @Override + public Iterable getListeners(ApplicationLoadBalancer alb) { + final ElasticLoadBalancingV2Client client = getLoadBalancingClient(getRegion(alb.getRegion())); + return client.describeListeners(b->b.loadBalancerArn(alb.getArn())).listeners(); + } + + @Override + public LoadBalancerState getApplicationLoadBalancerStatus(ApplicationLoadBalancer alb) { + final ElasticLoadBalancingV2Client client = getLoadBalancingClient(getRegion(alb.getRegion())); + final DescribeLoadBalancersResponse response = client.describeLoadBalancers(b->b.loadBalancerArns(alb.getArn())); + return response.loadBalancers().iterator().next().state(); + } + + @Override + public Iterable getLoadBalancerListenerRules(Listener loadBalancerListener, com.sap.sse.landscape.Region region) { + return getLoadBalancingClient(getRegion(region)).describeRules(b->b.listenerArn(loadBalancerListener.listenerArn())).rules(); + } + + @Override + public Iterable createLoadBalancerListenerRules(com.sap.sse.landscape.Region region, + Listener loadBalancerListenerToAddRuleTo, Rule... rulesToAdd) { + final List result = new ArrayList<>(); + for (final Rule rule : rulesToAdd) { + result.add(getLoadBalancingClient(getRegion(region)).createRule(b -> b + .listenerArn(loadBalancerListenerToAddRuleTo.listenerArn()) + .conditions(rule.conditions()) + .priority(Integer.valueOf(rule.priority())) + .actions(rule.actions())).rules().iterator().next()); + } + return result; } @Override - public void deleteLoadBalancer(ApplicationLoadBalancer alb) { - getLoadBalancingClient(getRegion(alb.getRegion())).deleteLoadBalancer(DeleteLoadBalancerRequest.builder().loadBalancerArn(alb.getArn()).build()); + public void deleteLoadBalancerListenerRules(com.sap.sse.landscape.Region region, Rule... rulesToDelete) { + for (final Rule rule : rulesToDelete) { + getLoadBalancingClient(getRegion(region)).deleteRule(b -> b.ruleArn(rule.ruleArn())); + } + } + + @Override + public void updateLoadBalancerListenerRulePriorities(com.sap.sse.landscape.Region region, Collection newRulePriorities) { + getLoadBalancingClient(getRegion(region)).setRulePriorities(SetRulePrioritiesRequest.builder().rulePriorities(newRulePriorities).build()); + } + + @Override + public void deleteLoadBalancerListener(com.sap.sse.landscape.Region region, Listener listener) { + getLoadBalancingClient(getRegion(region)).deleteListener(b->b.listenerArn(listener.listenerArn())); } /** @@ -195,20 +331,29 @@ ReplicaProcessT extends ApplicationReplicaProcess> getLoadBalancers(com.sap.sse.landscape.Region region) { + final List loadBalancers = getLoadBalancingClient(getRegion(region)) + .describeLoadBalancers(DescribeLoadBalancersRequest.builder().build()) + .loadBalancers(); + return Util.map(loadBalancers, lb->new ApplicationLoadBalancerImpl<>(region, lb, this)); + + } @Override - public ApplicationLoadBalancer getLoadBalancer(String loadBalancerArn, com.sap.sse.landscape.Region region) { + public ApplicationLoadBalancer getLoadBalancer(String loadBalancerArn, com.sap.sse.landscape.Region region) { final LoadBalancer loadBalancer = getLoadBalancingClient(getRegion(region)) .describeLoadBalancers(DescribeLoadBalancersRequest.builder().loadBalancerArns(loadBalancerArn).build()) .loadBalancers().iterator().next(); - return new ApplicationLoadBalancerImpl(region, loadBalancer); + return new ApplicationLoadBalancerImpl<>(region, loadBalancer, this); } @Override @@ -230,7 +375,7 @@ ReplicaProcessT extends ApplicationReplicaProcess alb) { final String dnsName = alb.getDNSName(); return setDNSRecord(hostedZoneId, hostname, RRType.CNAME, dnsName); } @@ -240,11 +385,9 @@ ReplicaProcessT extends ApplicationReplicaProcessb.dnsName(hostedZoneName)).hostedZones().iterator().next().id().replaceFirst("^\\/hostedzone\\/", ""); } private ChangeInfo setDNSRecord(String hostedZoneId, String hostname, RRType type, String value) { @@ -285,6 +428,19 @@ ReplicaProcessT extends ApplicationReplicaProcess(response.images().iterator().next(), region); } + @Override + public AmazonMachineImage getLatestImageWithTag(com.sap.sse.landscape.Region region, String tagName, String tagValue) { + final DescribeImagesResponse response = getEc2Client(getRegion(region)) + .describeImages(DescribeImagesRequest.builder().filters(Filter.builder().name("tag:"+tagName).values(tagValue).build()).build()); + return new AmazonMachineImage<>(response.images().stream().min(getMachineImageCreationDateComparator()).get(), region); + } + + private Comparator getMachineImageCreationDateComparator() { + return (ami1, ami2)->{ + return ami1.creationDate().compareTo(ami2.creationDate()); + }; + } + private AwsCredentials getCredentials() { return AwsBasicCredentials.create(accessKeyId, secretAccessKey); } @@ -325,7 +481,7 @@ ReplicaProcessT extends ApplicationReplicaProcess> launchHosts(int numberOfHostsToLaunch, MachineImage> fromImage, - InstanceType instanceType, AwsAvailabilityZone az, String keyName, Iterable securityGroups, String... userData) { + public Iterable> launchHosts(int numberOfHostsToLaunch, MachineImage fromImage, + InstanceType instanceType, AwsAvailabilityZone az, String keyName, Iterable securityGroups, Optional tags, String... userData) { if (!fromImage.getRegion().equals(az.getRegion())) { throw new IllegalArgumentException("Trying to launch an instance in region "+az.getRegion()+ " with image "+fromImage+" that lives in region "+fromImage.getRegion()+" which is different."+ " Consider copying the image to that region."); } - final RunInstancesRequest launchRequest = RunInstancesRequest.builder() + final Builder runInstancesRequestBuilder = RunInstancesRequest.builder() .additionalInfo("Test " + getClass().getName()) .imageId(fromImage.getId().toString()) .minCount(numberOfHostsToLaunch) .maxCount(numberOfHostsToLaunch) .instanceType(instanceType).keyName(keyName) .placement(Placement.builder().availabilityZone(az.getName()).build()) - .securityGroupIds(Util.mapToArrayList(securityGroups, sg->sg.getId())) - .userData(String.join("\n", userData)).build(); + .securityGroupIds(Util.mapToArrayList(securityGroups, sg->sg.getId())); + if (userData != null) { + runInstancesRequestBuilder.userData(String.join("\n", userData)); + } + tags.ifPresent(theTags->{ + final List awsTags = new ArrayList<>(); + for (final Entry tag : theTags) { + awsTags.add(Tag.builder().key(tag.getKey()).value(tag.getValue()).build()); + } + runInstancesRequestBuilder.tagSpecifications(TagSpecification.builder().resourceType(ResourceType.INSTANCE).tags(awsTags).build()); + }); + final RunInstancesRequest launchRequest = runInstancesRequestBuilder.build(); logger.info("Launching instance(s): "+launchRequest); final RunInstancesResponse response = getEc2Client(getRegion(az.getRegion())).runInstances(launchRequest); final List> result = new ArrayList<>(); @@ -381,7 +547,7 @@ ReplicaProcessT extends ApplicationReplicaProcess createTargetGroup(AwsRegion region, String targetGroupName, int port, String healthCheckPath, int healthCheckPort) { + public TargetGroup createTargetGroup(com.sap.sse.landscape.Region region, String targetGroupName, int port, String healthCheckPath, int healthCheckPort) { final ElasticLoadBalancingV2Client loadBalancingClient = getLoadBalancingClient(getRegion(region)); final software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup targetGroup = loadBalancingClient.createTargetGroup(CreateTargetGroupRequest.builder() @@ -393,9 +559,10 @@ ReplicaProcessT extends ApplicationReplicaProcess(this, region, targetGroupName, targetGroupArn); } + + private ProtocolEnum guessProtocolFromPort(int healthCheckPort) { + return healthCheckPort == 443 ? ProtocolEnum.HTTPS : ProtocolEnum.HTTP; + } - private String getVpcId(AwsRegion region) { + private String getVpcId(com.sap.sse.landscape.Region region) { Vpc vpc = getEc2Client(getRegion(region)).describeVpcs().vpcs().stream().filter(myVpc->myVpc.isDefault()).findAny(). orElseThrow(()->new IllegalStateException("No default VPC found in region "+region)); return vpc.vpcId(); } @Override - public software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroup(AwsRegion region, String targetGroupName) { + public software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroup(com.sap.sse.landscape.Region region, String targetGroupName) { return getLoadBalancingClient(getRegion(region)).describeTargetGroups(DescribeTargetGroupsRequest.builder() .names(targetGroupName).build()).targetGroups().iterator().next(); } @Override - public void deleteTargetGroup(TargetGroup targetGroup) { + public software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroupByArn(com.sap.sse.landscape.Region region, String targetGroupArn) { + return getLoadBalancingClient(getRegion(region)).describeTargetGroups(DescribeTargetGroupsRequest.builder() + .targetGroupArns(targetGroupArn).build()).targetGroups().iterator().next(); + } + + @Override + public void deleteTargetGroup(TargetGroup targetGroup) { getLoadBalancingClient(getRegion(targetGroup.getRegion())).deleteTargetGroup(DeleteTargetGroupRequest.builder().targetGroupArn( targetGroup.getTargetGroupArn()).build()); } @@ -437,4 +614,127 @@ ReplicaProcessT extends ApplicationReplicaProcess getCentralReverseProxy(com.sap.sse.landscape.Region region) { + return centralReverseProxyByRegion.get(region); + } + + @Override + public SecurityGroup getSecurityGroup(String securityGroupId, com.sap.sse.landscape.Region region) { + return ()->getEc2Client(getRegion(region)).describeSecurityGroups(sg->sg.groupIds(securityGroupId)).securityGroups().iterator().next().groupId(); + } + + @Override + public void addTargetsToTargetGroup( + TargetGroup targetGroup, + Iterable> targets) { + getLoadBalancingClient(getRegion(targetGroup.getRegion())).registerTargets(getRegisterTargetsRequestBuilderConsumer(targetGroup, targets)); + } + + private TargetDescription[] getTargetDescriptions(Iterable> targets) { + return Util.toArray(Util.map(targets, t->TargetDescription.builder().id(t.getInstanceId()).build()), new TargetDescription[0]); + } + + @Override + public void removeTargetsFromTargetGroup( + TargetGroup targetGroup, + Iterable> targets) { + getLoadBalancingClient(getRegion(targetGroup.getRegion())).deregisterTargets(getDeregisterTargetRequestBuilderConsumers(targetGroup, targets)); + } + + private Consumer getRegisterTargetsRequestBuilderConsumer( + TargetGroup targetGroup, Iterable> targets) { + final TargetDescription[] targetDescriptions = getTargetDescriptions(targets); + return b->b + .targetGroupArn(targetGroup.getTargetGroupArn()) + .targets(targetDescriptions); + } + + private Consumer getDeregisterTargetRequestBuilderConsumers( + TargetGroup targetGroup, Iterable> targets) { + final TargetDescription[] targetDescriptions = getTargetDescriptions(targets); + return b->b + .targetGroupArn(targetGroup.getTargetGroupArn()) + .targets(targetDescriptions); + } + + @Override + public LoadBalancer getAwsLoadBalancer(String loadBalancerArn, com.sap.sse.landscape.Region region) { + return getLoadBalancingClient(getRegion(region)) + .describeLoadBalancers(lb -> lb.loadBalancerArns(loadBalancerArn)).loadBalancers().iterator().next(); + } + + @Override + public SecurityGroup getDefaultSecurityGroupForApplicationHosts(com.sap.sse.landscape.Region region) { + final SecurityGroup result; + // TODO find a better way, e.g., by tagging, to identify the security group per region to use for application hosts + if (region.getId().equals(Region.EU_WEST_1.id())) { + result = getSecurityGroup(DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_1, region); + } else if (region.getId().equals(Region.EU_WEST_2.id())) { + result = getSecurityGroup(DEFAULT_APPLICATION_SERVER_SECURITY_GROUP_ID_EU_WEST_2, region); + } else { + result = null; + } + return result; + } + + @Override + public SecurityGroup getDefaultSecurityGroupForCentralReverseProxy(com.sap.sse.landscape.Region region) { + return getDefaultSecurityGroupForApplicationHosts(region); + } + + @Override + public ApplicationLoadBalancer getNonDNSMappedLoadBalancer( + com.sap.sse.landscape.Region region) { + final DescribeLoadBalancersResponse response = getLoadBalancingClient(getRegion(region)).describeLoadBalancers(b->b.names(DEFAULT_NON_DNS_MAPPED_ALB_NAME)); + return response.hasLoadBalancers() ? new ApplicationLoadBalancerImpl<>(region, response.loadBalancers().iterator().next(), this) : null; + } + + @Override + public ApplicationLoadBalancer getDNSMappedLoadBalancerFor( + com.sap.sse.landscape.Region region, String hostname) { + final DescribeLoadBalancersResponse response = getLoadBalancingClient(getRegion(region)).describeLoadBalancers(b->b.names(DEFAULT_NON_DNS_MAPPED_ALB_NAME)); + for (final LoadBalancer lb : response.loadBalancers()) { + final ApplicationLoadBalancer alb = new ApplicationLoadBalancerImpl<>(region, lb, this); + for (final Rule rule : alb.getRules()) { + if (rule.conditions().stream().filter(r->r.hostHeaderConfig().values().contains(hostname)).findAny().isPresent()) { + return alb; + } + } + } + return null; + } + + @Override + public MongoEndpoint getDatabaseConfigurationForDefaultCluster(com.sap.sse.landscape.Region region) { + // TODO Implement AwsLandscape.getDatabaseConfigurationForDefaultCluster(...) + return null; + } + + @Override + public Database getDatabase(com.sap.sse.landscape.Region region, String databaseName) { + return new DatabaseImpl(getDatabaseConfigurationForDefaultCluster(region), databaseName); + } + + @Override + public RabbitMQEndpoint getMessagingConfigurationForDefaultCluster(com.sap.sse.landscape.Region region) { + final RabbitMQEndpoint result; + if (region.getId().equals(Region.EU_WEST_1.id())) { + result = ()->"rabbit.internal.sapsailing.com"; + } else { + result = null; + } + return result; + } + + @Override + public com.sap.sse.landscape.Region getDefaultRegion() { + return new AwsRegion(Region.EU_WEST_2); // TODO actually, EU_WEST_1 (Ireland) is our default region, but as long as this is under development, EU_WEST_2 gives us an isolated test environment + } + + @Override + public Iterable getRegions() { + return Util.map(Arrays.asList(Region.EU_WEST_2, Region.CA_CENTRAL_1), AwsRegion::new); + } } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsTargetGroupImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsTargetGroupImpl.java index ec1234729ca..0664db97551 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsTargetGroupImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsTargetGroupImpl.java @@ -3,9 +3,11 @@ package com.sap.sse.landscape.aws.impl; import java.util.Map; import com.sap.sse.common.impl.NamedImpl; +import com.sap.sse.landscape.Region; import com.sap.sse.landscape.application.ApplicationMasterProcess; import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.ApplicationLoadBalancer; import com.sap.sse.landscape.aws.AwsInstance; import com.sap.sse.landscape.aws.AwsLandscape; import com.sap.sse.landscape.aws.TargetGroup; @@ -18,16 +20,20 @@ ReplicaProcessT extends ApplicationReplicaProcess landscape; - private final AwsRegion region; + private final Region region; - public AwsTargetGroupImpl(AwsLandscape landscape, AwsRegion region, String targetGroupName, String arn) { + public AwsTargetGroupImpl(AwsLandscape landscape, Region region, String targetGroupName, String arn) { super(targetGroupName); this.arn = arn; this.landscape = landscape; this.region = region; } + + private software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup getAwsTargetGroup() { + return landscape.getAwsTargetGroupByArn(getRegion(), getTargetGroupArn()); + } - public AwsRegion getRegion() { + public Region getRegion() { return region; } @@ -35,15 +41,28 @@ ReplicaProcessT extends ApplicationReplicaProcess, TargetHealth> getRegisteredTargets() { return landscape.getTargetHealthDescriptions(this); } + + @Override + public ApplicationLoadBalancer getLoadBalancer() { + final software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroup targetGroup = getAwsTargetGroup(); + final ApplicationLoadBalancer result; + if (targetGroup.hasLoadBalancerArns() && !targetGroup.loadBalancerArns().isEmpty()) { + result = new ApplicationLoadBalancerImpl<>(getRegion(), landscape.getAwsLoadBalancer( + targetGroup.loadBalancerArns().iterator().next(), getRegion()), landscape); + } else { + result = null; + } + return result; + } @Override public void addTargets(Iterable> targets) { - // TODO Implement AwsTargetGroupImpl.addTargets(...) + landscape.addTargetsToTargetGroup(this, targets); } @Override public void removeTargets(Iterable> targets) { - // TODO Implement AwsTargetGroupImpl.removeTargets(...) + landscape.removeTargetsFromTargetGroup(this, targets); } @Override diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/TagsImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/TagsImpl.java new file mode 100644 index 00000000000..29f11bde07d --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/TagsImpl.java @@ -0,0 +1,32 @@ +package com.sap.sse.landscape.aws.impl; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import com.sap.sse.landscape.aws.Tags; + +public class TagsImpl implements Tags { + private final Map tags; + + public TagsImpl() { + tags = new HashMap<>(); + } + + public TagsImpl(String key, String value) { + this(); + and(key, value); + } + + @Override + public Tags and(String key, String value) { + tags.put(key, value); + return this; + } + + @Override + public Iterator> iterator() { + return tags.entrySet().iterator(); + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateDNSBasedLoadBalancerMapping.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateDNSBasedLoadBalancerMapping.java new file mode 100644 index 00000000000..5898e2ec51b --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateDNSBasedLoadBalancerMapping.java @@ -0,0 +1,116 @@ +package com.sap.sse.landscape.aws.orchestration; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; +import com.sap.sse.common.Duration; +import com.sap.sse.common.Util; +import com.sap.sse.landscape.Region; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.ApplicationLoadBalancer; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.orchestration.Procedure; + +import software.amazon.awssdk.services.elasticloadbalancingv2.model.LoadBalancerStateEnum; + +/** + * For an {@link ApplicationProcess} creates a DNS-based load balancer set-up. This entails setting up a Route53 DNS entry + * for the hostname, pointing to the load balancer's {@link ApplicationLoadBalancer#getDNSName() DNS name} as a CNAME record. + * Furthermore, this procedure has logic to provision a new ALB in case no other DNS-mapped ALB has sufficient rule capacity + * (see {@link #MAX_RULES_PER_ALB}) to accommodate the {@link #NUMBER_OF_RULES_PER_REPLICA_SET} additional rules required. + * The naming scheme for those DNS-mapped ALBs is expected to follow the pattern {@link #DNS_MAPPED_ALB_NAME_PREFIX}{@code [0-9]*} which + * means that names such as "DNSMapped-4" and "DNSMapped-7" are valid names. + * + * @author Axel Uhl (D043530) + */ +public class CreateDNSBasedLoadBalancerMapping, +ReplicaProcessT extends ApplicationReplicaProcess, HostT extends AwsInstance> + extends CreateLoadBalancerMapping> + implements Procedure { + + private static final String DNS_MAPPED_ALB_NAME_PREFIX = "DNSMapped-"; + private static final Pattern ALB_NAME_PATTERN = Pattern.compile(DNS_MAPPED_ALB_NAME_PREFIX+"(.*)$"); + + public CreateDNSBasedLoadBalancerMapping( + ApplicationProcess process, String hostname, + String targetGroupNamePrefix, + AwsLandscape landscape, + Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException { + super(process, getOrCreateDNSMappedLoadBalancer(landscape, process.getHost().getRegion()), hostname, + targetGroupNamePrefix, landscape, optionalTimeout); + } + + /** + * Finds or creates a {@link ApplicationLoadBalander} load balancer in the {@code region} that is DNS-mapped and still has + * at least the length of {@link #createRules()} additional rules available. + * @throws InterruptedException + */ + private static , + ReplicaProcessT extends ApplicationReplicaProcess, HostT extends AwsInstance> + ApplicationLoadBalancer getOrCreateDNSMappedLoadBalancer( + AwsLandscape landscape, Region region) throws InterruptedException { + ApplicationLoadBalancer result = null; + final Set loadBalancerNames = new HashSet<>(); + for (final ApplicationLoadBalancer loadBalancer : landscape.getLoadBalancers(region)) { + if (ALB_NAME_PATTERN.matcher(loadBalancer.getName()).matches()) { + loadBalancerNames.add(loadBalancer.getName()); + if (Util.size(loadBalancer.getRules()) <= MAX_RULES_PER_ALB - NUMBER_OF_RULES_PER_REPLICA_SET) { + result = loadBalancer; + break; + } + } + } + if (result == null) { + result = landscape.createLoadBalancer(getAvailableDNSMappedAlbName(loadBalancerNames), region); + while (landscape.getApplicationLoadBalancerStatus(result).code() == LoadBalancerStateEnum.PROVISIONING) { + Thread.sleep(1000); // wait until the ALB has been provisioned or failed + } + } + return result; + } + + /** + * Picks a new load balancer name following the pattern {@link #DNS_MAPPED_ALB_NAME_PREFIX}{@code [0-9]+} that is not + * part of {@code loadBalancerNames} and has the least number. + */ + private static String getAvailableDNSMappedAlbName(Set loadBalancerNames) { + final Set numbersTaken = new HashSet<>(); + for (final String loadBalancerName : loadBalancerNames) { + final Matcher matcher = ALB_NAME_PATTERN.matcher(loadBalancerName); + if (matcher.find()) { + numbersTaken.add(Integer.parseInt(matcher.group(1))); + } + } + return DNS_MAPPED_ALB_NAME_PREFIX + IntStream.range(0, MAX_ALBS_PER_REGION).filter(i->!numbersTaken.contains(i)).min().getAsInt(); + } + + @Override + public void run() throws JSchException, IOException, InterruptedException, SftpException { + super.run(); + createRoute53Mapping(); + } + + private void createRoute53Mapping() { + final String hostname = this.getHostName(); + final ApplicationLoadBalancer alb = getLoadBalancerUsed(); + getLandscape().setDNSRecordToApplicationLoadBalancer(getLandscape().getDNSHostedZoneId( + getHostedZoneName(hostname)), hostname, alb); + } + + private String getHostedZoneName(String hostname) { + return hostname.substring(hostname.indexOf('.')+1); + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateDynamicLoadBalancerMapping.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateDynamicLoadBalancerMapping.java new file mode 100644 index 00000000000..5c5f1bae1dc --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateDynamicLoadBalancerMapping.java @@ -0,0 +1,34 @@ +package com.sap.sse.landscape.aws.orchestration; + +import java.io.IOException; +import java.util.Optional; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.orchestration.Procedure; + +/** + * For an {@link ApplicationProcess} creates a "dynamic" load balancer set-up. + * @author Axel Uhl (D043530) + */ +public class CreateDynamicLoadBalancerMapping, +ReplicaProcessT extends ApplicationReplicaProcess, HostT extends AwsInstance> + extends CreateLoadBalancerMapping> + implements Procedure { + public CreateDynamicLoadBalancerMapping( + ApplicationProcess process, String hostname, + String targetGroupNamePrefix, + AwsLandscape landscape, + Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException { + super(process, landscape.getNonDNSMappedLoadBalancer(process.getHost().getRegion()), hostname, + targetGroupNamePrefix, landscape, optionalTimeout); + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java new file mode 100644 index 00000000000..dfe2545ee52 --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java @@ -0,0 +1,184 @@ +package com.sap.sse.landscape.aws.orchestration; + +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; +import com.sap.sse.common.Duration; +import com.sap.sse.common.HttpRequestHeaderConstants; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.ApplicationLoadBalancer; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.TargetGroup; +import com.sap.sse.landscape.orchestration.AbstractProcedureImpl; + +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Action; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.ActionTypeEnum; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.RuleCondition; +import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroupTuple; + +/** + * For an {@link ApplicationProcess} creates a set of rules in an {@link ApplicationLoadBalancer} which drives traffic + * to one of the two {@link TargetGroup}s that this procedure will also create. One will take the traffic for the single + * "master" node; the other will take the traffic for all public-facing nodes which by default in the minimal + * application server replica set configuration will be the single master node. As the number of replicas grows, the + * master may choose to only serve the writing requests and be removed from the public-facing target group again. + *

+ * + * The rules that this procedure creates are currently the following: + *

    + *
  1. If the HTTP header field identified by {@link HttpRequestHeaderConstants#HEADER_KEY_FORWARD_TO} has value + * {@link HttpRequestHeaderConstants#HEADER_FORWARD_TO_MASTER} and the hostname header matches then forward to the + * "master" target group
  2. + *
  3. If the HTTP header field identified by {@link HttpRequestHeaderConstants#HEADER_KEY_FORWARD_TO} has value + * {@link HttpRequestHeaderConstants#HEADER_FORWARD_TO_REPLICA} and the hostname header matches then forward to the + * "public" target group
  4. + *
  5. If the HTTP request method is {@code GET} and the hostname header matches then forward to the "public" target + * group
  6. + *
  7. Forward all other requests with a matching hostname header to the "master" target group
  8. + *
+ *

+ * + * The target groups are set up to use HTTP as the protocol such that SSL offloading will happen at the load balancer. + * The target group names are limited to 32 characters in length. Target group name prefixes should be chosen to be + * very short strings in order not to unnecessarily limit the number of characters available for application server + * replica set naming. + *

+ * + * The {@link ApplicationProcess#getPort() port} and the {@link ApplicationProcess#getHealthCheckPath() health check + * path} are taken from the {@link ApplicationProcess}. The application process's {@link ApplicationProcess#getHost() + * host} is then added to both target groups. + *

+ * + * The default health check settings for the target groups created are: + *

    + *
  • healthy threshold: 2
  • + *
  • unhealthy threshold: 2
  • + *
  • timeout: 4s
  • + *
  • interval: 5s
  • + *
  • success codes: 200
  • + *
+ * + * @author Axel Uhl (D043530) + */ +public abstract class CreateLoadBalancerMapping, +ReplicaProcessT extends ApplicationReplicaProcess, HostT extends AwsInstance> + extends AbstractProcedureImpl { + private static final String MASTER_TARGET_GROUP_SUFFIX = "-m"; + protected static int NUMBER_OF_RULES_PER_REPLICA_SET = 4; + protected static final int MAX_RULES_PER_ALB = 100; + protected static final int MAX_ALBS_PER_REGION = 20; + private final ApplicationProcess process; + private final ApplicationLoadBalancer loadBalancerUsed; + private final String targetGroupNamePrefix; + private final String hostname; + private final String servername; + private final Optional optionalTimeout; + private TargetGroup masterTargetGroupCreated; + private TargetGroup publicTargetGroupCreated; + private Iterable rulesAdded; + + public CreateLoadBalancerMapping(ApplicationProcess process, + ApplicationLoadBalancer loadBalancerUsed, String hostname, + String targetGroupNamePrefix, + AwsLandscape landscape, + Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException { + super(landscape); + this.loadBalancerUsed = loadBalancerUsed; + this.targetGroupNamePrefix = targetGroupNamePrefix; + this.process = process; + this.hostname = hostname; + this.optionalTimeout = optionalTimeout; + this.servername = process.getServerName(optionalTimeout); + } + + @Override + public AwsLandscape getLandscape() { + return (AwsLandscape) super.getLandscape(); + } + + @Override + public void run() throws JSchException, IOException, InterruptedException, SftpException { + masterTargetGroupCreated = getLandscape().createTargetGroup(loadBalancerUsed.getRegion(), + getMasterTargetGroupName(optionalTimeout), getProcess().getPort(), getProcess().getHealthCheckPath(), + /* use traffic port as health check port, too */ getProcess().getPort()); + publicTargetGroupCreated = getLandscape().createTargetGroup(loadBalancerUsed.getRegion(), + getPublicTargetGroupName(optionalTimeout), getProcess().getPort(), getProcess().getHealthCheckPath(), + /* use traffic port as health check port, too */ getProcess().getPort()); + getLandscape().addTargetsToTargetGroup(masterTargetGroupCreated, Collections.singleton(getHost())); + getLandscape().addTargetsToTargetGroup(publicTargetGroupCreated, Collections.singleton(getHost())); + getLoadBalancerUsed().addRulesAssigningUnusedPriorities(/* forceContiguous */ true, createRules()); + } + + private Rule[] createRules() { + final Rule[] rules = new Rule[NUMBER_OF_RULES_PER_REPLICA_SET]; + int ruleCount = 0; + rules[ruleCount++] = Rule.builder().conditions( + RuleCondition.builder().field("http-header").httpHeaderConfig(hhcb->hhcb.httpHeaderName(HttpRequestHeaderConstants.HEADER_KEY_FORWARD_TO).values(HttpRequestHeaderConstants.HEADER_FORWARD_TO_MASTER.getA())).build(), + RuleCondition.builder().field("host-header").hostHeaderConfig(hhcb->hhcb.values(getHostName())).build()). + actions(Action.builder().type(ActionTypeEnum.FORWARD).forwardConfig(fc->fc.targetGroups(TargetGroupTuple.builder().targetGroupArn((getMasterTargetGroupCreated().getTargetGroupArn())).build())).build()). + build(); + rules[ruleCount++] = Rule.builder().conditions( + RuleCondition.builder().field("http-header").httpHeaderConfig(hhcb->hhcb.httpHeaderName(HttpRequestHeaderConstants.HEADER_KEY_FORWARD_TO).values(HttpRequestHeaderConstants.HEADER_FORWARD_TO_REPLICA.getA())).build(), + RuleCondition.builder().field("host-header").hostHeaderConfig(hhcb->hhcb.values(getHostName())).build()). + actions(Action.builder().type(ActionTypeEnum.FORWARD).forwardConfig(fc->fc.targetGroups(TargetGroupTuple.builder().targetGroupArn((getPublicTargetGroupCreated().getTargetGroupArn())).build())).build()). + build(); + rules[ruleCount++] = Rule.builder().conditions( + RuleCondition.builder().field("http-request-method").httpRequestMethodConfig(hrmcb->hrmcb.values("GET")).build(), + RuleCondition.builder().field("host-header").hostHeaderConfig(hhcb->hhcb.values(getHostName())).build()). + actions(Action.builder().type(ActionTypeEnum.FORWARD).forwardConfig(fc->fc.targetGroups(TargetGroupTuple.builder().targetGroupArn((getPublicTargetGroupCreated().getTargetGroupArn())).build())).build()). + build(); + rules[ruleCount++] = Rule.builder().conditions( + RuleCondition.builder().field("host-header").hostHeaderConfig(hhcb->hhcb.values(getHostName())).build()). + actions(Action.builder().type(ActionTypeEnum.FORWARD).forwardConfig(fc->fc.targetGroups(TargetGroupTuple.builder().targetGroupArn((getMasterTargetGroupCreated().getTargetGroupArn())).build())).build()). + build(); + assert ruleCount == NUMBER_OF_RULES_PER_REPLICA_SET; + return rules; + } + + protected String getHostName() { + return hostname; + } + + private AwsInstance getHost() { + @SuppressWarnings("unchecked") + final AwsInstance result = (AwsInstance) getProcess().getHost(); + return result; + } + + private String getMasterTargetGroupName(Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException { + return getPublicTargetGroupName(optionalTimeout)+MASTER_TARGET_GROUP_SUFFIX; + } + + private String getPublicTargetGroupName(Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException { + return targetGroupNamePrefix+servername; + } + + public ApplicationLoadBalancer getLoadBalancerUsed() { + return loadBalancerUsed; + } + + public TargetGroup getMasterTargetGroupCreated() { + return masterTargetGroupCreated; + } + + public TargetGroup getPublicTargetGroupCreated() { + return publicTargetGroupCreated; + } + + public Iterable getRulesAdded() { + return rulesAdded; + } + + public ApplicationProcess getProcess() { + return process; + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java new file mode 100644 index 00000000000..d1715852aa7 --- /dev/null +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/StartAwsHost.java @@ -0,0 +1,569 @@ +package com.sap.sse.landscape.aws.orchestration; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +import com.sap.sse.common.Duration; +import com.sap.sse.common.Util; +import com.sap.sse.landscape.AvailabilityZone; +import com.sap.sse.landscape.Landscape; +import com.sap.sse.landscape.ProcessConfigurationVariable; +import com.sap.sse.landscape.Region; +import com.sap.sse.landscape.Release; +import com.sap.sse.landscape.ReplicationConfiguration; +import com.sap.sse.landscape.SecurityGroup; +import com.sap.sse.landscape.UserDataProvider; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.aws.AwsAvailabilityZone; +import com.sap.sse.landscape.aws.AwsInstance; +import com.sap.sse.landscape.aws.AwsLandscape; +import com.sap.sse.landscape.aws.Tags; +import com.sap.sse.landscape.aws.impl.AmazonMachineImage; +import com.sap.sse.landscape.aws.impl.AwsRegion; +import com.sap.sse.landscape.mongodb.Database; +import com.sap.sse.landscape.orchestration.StartHost; +import com.sap.sse.landscape.rabbitmq.RabbitMQEndpoint; + +import software.amazon.awssdk.services.ec2.model.InstanceType; + +/** + * Launches an AWS host when {@link #run() run}. The resulting {@link AwsInstance} can then be obtained by + * calling {@link #getHost()}. + * + * @author Axel Uhl (D043530) + */ +public abstract class StartAwsHost, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> +extends StartHost { + /** + * The {@link AwsLandscape#getLatestImageWithTag(com.sap.sse.landscape.Region, String, String)} method is + * used to obtain default images for specific AWS host starting procedures that subclass this class. The + * Amazon Machine Images (AMIs) for this are then expected to be tagged with a tag named as specified by this + * constant ("image-type"). The tag value then must match what the subclass wants. + * + * @see #getLatestImageOfType(String) + */ + private final static String IMAGE_TYPE_TAG_NAME = "image-type"; + + private final List userData; + private final InstanceType instanceType; + private final AwsAvailabilityZone availabilityZone; + private final String keyName; + private final Iterable securityGroups; + private final Optional tags; + private HostT host; + + /** + * A builder that helps building an instance of type {@link StartAwsHost} or any subclass thereof (then using + * specialized builders). The following default rules apply: + *
    + *
  • If an {@link #getImageType() image type} has been specified, it serves as the default for looking up the + * {@link #getMachineImage() image} to launch from. However, an image type set explicitly, or subclasses overriding + * {@link #getMachineImage()} may take precedence.
  • + *
  • If no {@link #setRelease(Release) release is set}, an empty {@link Optional} will be returned by + * {@link #getRelease()}, indicating to use the default release pre-deployed in the image launched.
  • + *
  • If no {@link #setAvailabilityZone(AwsAvailabilityZone) availability zone} is specified, this builder will try + * to obtain the {@link #getRegion()} which in this case must have been {@link #setRegion(AwsRegion)} or otherwise + * be returned from an overridden {@link #getRegion()} in a specialized builder, and will + * {@link StartAwsHost#getRandomAvailabilityZone(AwsRegion, AwsLandscape) pick an availability zone randomly} within + * that region.
  • + *
  • Conversely, if an {@link #setAvailabilityZone(AwsAvailabilityZone) availability zone has been set}, its + * {@link AwsAvailabilityZone#getRegion() region} will be the default answer of {@link #getRegion()}.
  • + *
  • If no {@link #setSecurityGroups(Iterable) security group} has been set, the {@link #getLandscape() landscape} + * is asked to provide its {@link AwsLandscape#getDefaultSecurityGroupForApplicationHosts(Region) default security + * group for application hosts} in the {@link #getRegion() region} used by this builder.
  • + *
  • If no {@link #setDatabaseName(String) database name is set explicitly}, it defaults to the + * {@link #getServerName() server name}.
  • + *
  • The {@link #getOptionalTimeout() optional timeout} defaults to an {@link Optional#empty() empty optional}, meaning + * that waiting for the instance won't timeout by default.
  • + *
+ * + * @author Axel Uhl (D043530) + */ + public static interface Builder, ShardingKey, + MetricsT extends ApplicationProcessMetrics, + MasterProcessT extends ApplicationMasterProcess, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> + extends StartHost.Builder { + @Override + AmazonMachineImage getMachineImage(); + + Builder setMachineImage(AmazonMachineImage machineImage); + + /** + * When not {@code null}, the newest {@link AmazonMachineImage} tagged with a tag named as specified by the constant {@link #IMAGE_TYPE_TAG_NAME} + * with the value provided by the result of this method will be searched and will be used as the default for {@link #getMachineImage()}. + */ + String getImageType(); + + Builder setImageType(String imageType); + + /** + * By default, the release pre-deployed in the image will be used, represented by an empty {@link Optional} + * returned by this default method implementation. + */ + Optional getRelease(); + + Builder setRelease(Optional release); + + @Override + AwsLandscape getLandscape(); + + Builder setLandscape(AwsLandscape landscape); + + InstanceType getInstanceType(); + + Builder setInstanceType(InstanceType instanceType); + + AwsAvailabilityZone getAvailabilityZone(); + + Builder setAvailabilityZone(AwsAvailabilityZone availabilityZone); + + String getKeyName(); + + Builder setKeyName(String keyName); + + Iterable getSecurityGroups(); + + Builder setSecurityGroups(Iterable securityGroups); + + Optional getTags(); + + Builder setTags(Optional tags); + + String[] getUserData(); + + Builder setUserData(String[] userData); + + AwsRegion getRegion(); + + Builder setRegion(AwsRegion region); + + String getInstanceName(); + + Builder setInstanceName(String name); + + String getServerName(); + + Builder setServerName(String serverName); + + String getDatabaseName(); + + Builder setDatabaseName(String databaseName); + + Optional getReplicationConfiguration(); + + Builder setReplicationConfiguration( + Optional replicationConfiguration); + + String getOutputReplicationExchangeName(); + + Builder setOutputReplicationExchangeName(String outputReplicationExchangeName); + + RabbitMQEndpoint getRabbitConfiguration(); + + Builder setRabbitConfiguration(RabbitMQEndpoint rabbitConfiguration); + + Database getDatabaseConfiguration(); + + Builder setDatabaseConfiguration(Database databaseConfiguration); + + String getCommaSeparatedEmailAddressesToNotifyOfStartup(); + + Builder setCommaSeparatedEmailAddressesToNotifyOfStartup( + String commaSeparatedEmailAddressesToNotifyOfStartup); + + /** + * A timeout for interacting with the instance, such as when creating an SSH / SFTP connection or waiting for its + * public IP address. + */ + Optional getOptionalTimeout(); + + Builder setOptionalTimeout(Optional optionalTimeout); + } + + protected abstract static class BuilderImpl, ShardingKey, + MetricsT extends ApplicationProcessMetrics, + MasterProcessT extends ApplicationMasterProcess, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends AwsInstance> implements Builder { + private AmazonMachineImage machineImage; + private String imageType; + private Optional release = Optional.empty(); + private AwsLandscape landscape; + private InstanceType instanceType; + private AwsAvailabilityZone availabilityZone; + private String keyName; + private Iterable securityGroups; + private Optional tags = Optional.empty(); + private String[] userData; + private AwsRegion region; + private String instanceName; + private String serverName; + private String databaseName; + private Database databaseConfiguration; + private RabbitMQEndpoint rabbitConfiguration; + private String outputReplicationExchangeName; + private Optional replicationConfiguration; + private String commaSeparatedEmailAddressesToNotifyOfStartup; + private Optional optionalTimeout; + + @Override + public AmazonMachineImage getMachineImage() { + return machineImage == null ? getLatestImageOfType(getImageType(), getLandscape(), getRegion()) : machineImage; + } + + @Override + public Builder setMachineImage( + AmazonMachineImage machineImage) { + this.machineImage = machineImage; + return this; + } + + @Override + public String getImageType() { + return imageType; + } + + @Override + public Builder setImageType( + String imageType) { + this.imageType = imageType; + return this; + } + + @Override + public Optional getRelease() { + return release; + } + + @Override + public Builder setRelease(Optional release) { + this.release = release; + return this; + } + + @Override + public AwsLandscape getLandscape() { + return landscape; + } + + @Override + public Builder setLandscape( + AwsLandscape landscape) { + this.landscape = landscape; + return this; + } + + @Override + public InstanceType getInstanceType() { + return instanceType; + } + + @Override + public Builder setInstanceType( + InstanceType instanceType) { + this.instanceType = instanceType; + return this; + } + + @Override + public AwsAvailabilityZone getAvailabilityZone() { + return availabilityZone == null ? getRandomAvailabilityZone(getRegion(), getLandscape()) : availabilityZone; + } + + @Override + public Builder setAvailabilityZone( + AwsAvailabilityZone availabilityZone) { + this.availabilityZone = availabilityZone; + return this; + } + + @Override + public String getKeyName() { + return keyName; + } + + @Override + public Builder setKeyName(String keyName) { + this.keyName = keyName; + return this; + } + + @Override + public Iterable getSecurityGroups() { + return securityGroups == null ? Collections.singleton(getLandscape().getDefaultSecurityGroupForApplicationHosts(getRegion())) : securityGroups; + } + + @Override + public Builder setSecurityGroups( + Iterable securityGroups) { + this.securityGroups = securityGroups; + return this; + } + + @Override + public Optional getTags() { + return tags; + } + + @Override + public Builder setTags(Optional tags) { + this.tags = tags; + return this; + } + + @Override + public String[] getUserData() { + return userData; + } + + @Override + public Builder setUserData( + String[] userData) { + this.userData = userData; + return this; + } + + @Override + public AwsRegion getRegion() { + return region == null ? getAvailabilityZone().getRegion() : region; + } + + @Override + public Builder setRegion(AwsRegion region) { + this.region = region; + return this; + } + + @Override + public String getInstanceName() { + return instanceName; + } + + @Override + public Builder setInstanceName(String instanceName) { + this.instanceName = instanceName; + return this; + } + + @Override + public String getServerName() { + return serverName; + } + + @Override + public Builder setServerName(String serverName) { + this.serverName = serverName; + return this; + } + + @Override + public String getDatabaseName() { + return databaseName == null ? getServerName() : databaseName; + } + + @Override + public Builder setDatabaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + @Override + public Database getDatabaseConfiguration() { + return databaseConfiguration == null ? getLandscape().getDatabase(getRegion(), getDatabaseName()) : databaseConfiguration; + } + + @Override + public Builder setDatabaseConfiguration(Database databaseConfiguration) { + this.databaseConfiguration = databaseConfiguration; + return this; + } + + @Override + public RabbitMQEndpoint getRabbitConfiguration() { + return rabbitConfiguration; + } + + @Override + public Builder setRabbitConfiguration(RabbitMQEndpoint rabbitConfiguration) { + this.rabbitConfiguration = rabbitConfiguration; + return this; + } + + @Override + public String getOutputReplicationExchangeName() { + return outputReplicationExchangeName; + } + + @Override + public Builder setOutputReplicationExchangeName(String outputReplicationExchangeName) { + this.outputReplicationExchangeName = outputReplicationExchangeName; + return this; + } + + @Override + public Optional getReplicationConfiguration() { + return replicationConfiguration; + } + + @Override + public Builder setReplicationConfiguration(Optional replicationConfiguration) { + this.replicationConfiguration = replicationConfiguration; + return this; + } + + @Override + public String getCommaSeparatedEmailAddressesToNotifyOfStartup() { + return commaSeparatedEmailAddressesToNotifyOfStartup; + } + + @Override + public Builder setCommaSeparatedEmailAddressesToNotifyOfStartup(String commaSeparatedEmailAddressesToNotifyOfStartup) { + this.commaSeparatedEmailAddressesToNotifyOfStartup = commaSeparatedEmailAddressesToNotifyOfStartup; + return this; + } + + @Override + public Optional getOptionalTimeout() { + return optionalTimeout == null ? Optional.empty() : optionalTimeout; + } + + @Override + public Builder setOptionalTimeout( + Optional optionalTimeout) { + this.optionalTimeout = optionalTimeout; + return this; + } + } + + protected StartAwsHost(Builder, ShardingKey, MetricsT, MasterProcessT, ReplicaProcessT, HostT> builder) { + super(builder); + this.userData = new ArrayList<>(); + for (final String ud : builder.getUserData()) { + this.userData.add(ud); + } + this.instanceType = builder.getInstanceType(); + this.availabilityZone = builder.getAvailabilityZone(); + this.keyName = builder.getKeyName(); + this.tags = builder.getTags(); + this.securityGroups = builder.getSecurityGroups(); + builder.getRelease().ifPresent(this::addUserData); + addUserData(builder.getDatabaseConfiguration()); + addUserData(builder.getRabbitConfiguration()); + addUserData(ProcessConfigurationVariable.SERVER_NAME, builder.getServerName()); + addUserData(ProcessConfigurationVariable.REPLICATION_CHANNEL, builder.getOutputReplicationExchangeName()); + addUserData(ProcessConfigurationVariable.SERVER_STARTUP_NOTIFY, builder.getCommaSeparatedEmailAddressesToNotifyOfStartup()); + builder.getReplicationConfiguration().ifPresent(this::addUserData); + } + + protected static , + ReplicaProcessT extends ApplicationReplicaProcess> + AmazonMachineImage getLatestImageOfType(String imageType, AwsLandscape landscape, Region region) { + return landscape.getLatestImageWithTag(region, IMAGE_TYPE_TAG_NAME, imageType); + } + + protected static , + ReplicaProcessT extends ApplicationReplicaProcess> + AwsAvailabilityZone getRandomAvailabilityZone(AwsRegion region, AwsLandscape landscape) { + final Iterable azs = landscape.getAvailabilityZones(region); + return (AwsAvailabilityZone) Util.get(azs, new Random().nextInt(Util.size(azs))); + } + + protected static , + ReplicaProcessT extends ApplicationReplicaProcess> + Set getDefaultSecurityGroupForApplicationHosts(Landscape landscape, Region region) { + return Collections.singleton(landscape.getDefaultSecurityGroupForApplicationHosts(region)); + } + + protected static Optional addNameTag(String name, Optional tags) { + return Optional.of(tags.orElse(Tags.empty()).and("Name", name)); + } + + @Override + public AwsLandscape getLandscape() { + return (AwsLandscape) super.getLandscape(); + } + + @Override + public void run() throws Exception { + @SuppressWarnings({ "unchecked" }) + HostT castHost = (HostT) getLandscape().launchHost(getMachineImage(), getInstanceType(), getAvailabilityZone(), getKeyName(), getSecurityGroups(), getTags(), + Util.toArray(getUserData(), new String[0])); + host = castHost; + } + + /** + * @return {@code null} before {@link #run()} is called; the host launched afterwards + */ + public HostT getHost() { + return host; + } + + private Optional getTags() { + return tags; + } + + private Iterable getSecurityGroups() { + return securityGroups; + } + + private String getKeyName() { + return keyName; + } + + private AwsAvailabilityZone getAvailabilityZone() { + return availabilityZone; + } + + private InstanceType getInstanceType() { + return instanceType; + } + + /** + * @return the user data to pass to EC2 when launching the instance; based on the {@link #userData} passed to this + * object's constructor plus extensions added by calling {@link #addUserData}. Subclasses may override this + * method to change or add to these user data. + */ + protected Iterable getUserData() throws URISyntaxException { + return userData; + } + + /** + * Appends {@code moreUserData} to the end of {@link #userData} + */ + protected void addUserData(Iterable moreUserData) { + Util.addAll(moreUserData, userData); + } + + protected void addUserData(ProcessConfigurationVariable userDataVariable, String value) { + userData.add(userDataVariable.name()+"="+value); + } + + /** + * Appends {@code moreUserData} to the end of {@link #userData} + */ + protected void addUserData(UserDataProvider userDataProvider) { + for (final Entry userDataEntry : userDataProvider.getUserData().entrySet()) { + addUserData(userDataEntry.getKey(), userDataEntry.getValue()); + } + } +} diff --git a/java/com.sap.sse.landscape/META-INF/MANIFEST.MF b/java/com.sap.sse.landscape/META-INF/MANIFEST.MF index c5aa282a8af..b2deeb986a3 100755 --- a/java/com.sap.sse.landscape/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.landscape/META-INF/MANIFEST.MF @@ -8,8 +8,15 @@ Automatic-Module-Name: com.sap.sse.landscape Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Export-Package: com.sap.sse.landscape, com.sap.sse.landscape.application, + com.sap.sse.landscape.application.impl, + com.sap.sse.landscape.impl, com.sap.sse.landscape.mongodb, + com.sap.sse.landscape.mongodb.impl;x-friends:="com.sap.sse.landscape.aws", + com.sap.sse.landscape.orchestration, + com.sap.sse.landscape.orchestration.impl, com.sap.sse.landscape.rabbitmq, com.sap.sse.landscape.ssh Require-Bundle: com.sap.sse.common, - com.jcraft.jsch;bundle-version="0.1.54" + com.jcraft.jsch;bundle-version="0.1.54", + org.mongodb.mongo-java-driver +Import-Package: com.sap.sse.concurrent diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Host.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Host.java index 8a2c18bc539..f54fc385b84 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Host.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Host.java @@ -1,14 +1,23 @@ package com.sap.sse.landscape; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.net.InetAddress; +import java.util.Optional; +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSchException; import com.sap.sse.common.Duration; +import com.sap.sse.landscape.ssh.SshCommandChannel; public interface Host { /** * Obtains the public IP address of this host. Note that during the boot phase a host may not yet have such * a public IP address assigned. In this case, {@code null} may be returned. To avoid this, you can alternatively - * call {@link #getPublicAddress(long)} to wait for a public IP address to become available. + * call {@link #getPublicAddress(Optional)} to wait for a public IP address to become available. */ InetAddress getPublicAddress(); @@ -19,23 +28,41 @@ public interface Host { * @param timeoutNullMeaningForever * if {@code null}, waits forever */ - InetAddress getPublicAddress(Duration timeoutNullMeaningForever); + InetAddress getPublicAddress(Optional timeoutNullMeaningForever); + + /** + * Connects to an SSH session for the username specified, using the SSH key pair used to launch the instance, and + * opens a "shell" channel. Use the {@link Channel} returned by {@link Channel#setInputStream(java.io.InputStream) + * setting an input stream} from which the commands to be sent to the server will be read, and by + * {@link Channel#setOutputStream(java.io.OutputStream) setting the output stream} to which the server will send its + * output. You will usually want to use either a {@link ByteArrayInputStream} to provide a set of predefined + * commands to sent to the server, and a {@link PipedInputStream} wrapped around a {@link PipedOutputStream} which + * you set to the channel. + */ + SshCommandChannel createSshChannel(String sshUserName, Optional optionalTimeout) throws JSchException, IOException, InterruptedException; + /** + * Connects to an SSH session for the "root" user with a "shell" channel + * + * @see #createSshChannel(String, Optional) + */ + SshCommandChannel createRootSshChannel(Optional optionalTimeout) throws JSchException, IOException, InterruptedException; + + ChannelSftp createSftpChannel(String sshUserName, Optional optionalTimeout) throws JSchException, IOException; + + ChannelSftp createRootSftpChannel(Optional optionalTimeout) throws JSchException, IOException; + Iterable> getRunningProcesses(); - long getPhysicalRamInBytes(); - - long getVirtualMemoryInBytes(); - - int getNumberOfCPUs(); - - long getNetworkBandwidthInBytesPerSecond(); - /** * Tells where in the cloud this host runs; the availability zone {@link AvailabilityZone#getRegion() implies} the * {@link Region}. */ AvailabilityZone getAvailabilityZone(); + default Region getRegion() { + return getAvailabilityZone().getRegion(); + } + Iterable getSecurityGroups(); } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/InstanceType.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/InstanceType.java deleted file mode 100755 index f48bc4df48b..00000000000 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/InstanceType.java +++ /dev/null @@ -1,272 +0,0 @@ -package com.sap.sse.landscape; - -public enum InstanceType { - t2_nano, - t2_micro, - t2_small, - t2_medium, - t2_large, - t2_xlarge, - t2_2xlarge, - t3a_nano, - t3a_micro, - t3a_small, - t3a_medium, - t3a_large, - t3a_xlarge, - t3a_2xlarge, - t3_nano, - t3_micro, - t3_small, - t3_medium, - t3_large, - t3_xlarge, - t3_2xlarge, - m5ad_large, - m5ad_xlarge, - m5ad_2xlarge, - m5ad_4xlarge, - m5ad_8xlarge, - m5ad_12xlarge, - m5ad_16xlarge, - m5ad_24xlarge, - m5a_large, - m5a_xlarge, - m5a_2xlarge, - m5a_4xlarge, - m5a_8xlarge, - m5a_12xlarge, - m5a_16xlarge, - m5a_24xlarge, - m5d_large, - m5d_xlarge, - m5d_2xlarge, - m5d_4xlarge, - m5d_8xlarge, - m5d_12xlarge, - m5d_16xlarge, - m5d_24xlarge, - m5d_metal, - m5n_large, - m5n_xlarge, - m5n_2xlarge, - m5n_4xlarge, - m5n_8xlarge, - m5n_12xlarge, - m5n_16xlarge, - m5dn_large, - m5dn_xlarge, - m5dn_2xlarge, - m5dn_4xlarge, - m5dn_8xlarge, - m5dn_12xlarge, - m5dn_16xlarge, - m5n_24xlarge, - m5dn_24xlarge, - r5n_large, - r5n_xlarge, - r5n_2xlarge, - r5n_4xlarge, - r5n_8xlarge, - r5n_12xlarge, - r5n_16xlarge, - r5dn_large, - r5dn_xlarge, - r5dn_2xlarge, - r5dn_4xlarge, - r5dn_8xlarge, - r5dn_12xlarge, - r5dn_16xlarge, - r5n_24xlarge, - r5dn_24xlarge, - m5_large, - m5_xlarge, - m5_2xlarge, - m5_4xlarge, - m5_8xlarge, - m5_12xlarge, - m5_16xlarge, - m5_24xlarge, - m5_metal, - m4_large, - m4_xlarge, - m4_2xlarge, - m4_4xlarge, - m4_10xlarge, - m4_16xlarge, -// a1_medium, -// a1_large, -// a1_xlarge, -// a1_2xlarge, -// a1_4xlarge, -// a1_metal, -// m6g_medium, -// m6g_large, -// m6g_xlarge, -// m6g_2xlarge, -// m6g_4xlarge, -// m6g_8xlarge, -// m6g_12xlarge, -// m6g_16xlarge, -// m6g_metal, -// c6g_medium, -// c6g_large, -// c6g_xlarge, -// c6g_2xlarge, -// c6g_4xlarge, -// c6g_8xlarge, -// c6g_12xlarge, -// c6g_16xlarge, -// c6g_metal, -// r6g_medium, -// r6g_large, -// r6g_xlarge, -// r6g_2xlarge, -// r6g_4xlarge, -// r6g_8xlarge, -// r6g_12xlarge, -// r6g_16xlarge, -// r6g_metal, - c5n_large, - c5n_xlarge, - c5n_2xlarge, - c5n_4xlarge, - c5n_9xlarge, - c5n_18xlarge, - c5n_metal, - c5d_large, - c5d_xlarge, - c5d_2xlarge, - c5d_4xlarge, - c5d_9xlarge, - c5d_12xlarge, - c5d_18xlarge, - c5d_24xlarge, - c5d_metal, - c5_large, - c5_xlarge, - c5_2xlarge, - c5_4xlarge, - c5_9xlarge, - c5_12xlarge, - c5_18xlarge, - c5_24xlarge, - c5_metal, - c5a_large, - c5a_xlarge, - c5a_2xlarge, - c5a_4xlarge, - c5a_8xlarge, - c5a_12xlarge, - c5a_16xlarge, - c5a_24xlarge, - c4_large, - c4_xlarge, - c4_2xlarge, - c4_4xlarge, - c4_8xlarge, - f1_2xlarge, - f1_4xlarge, - f1_16xlarge, - g2_2xlarge, - g2_8xlarge, - g3s_xlarge, - g3_4xlarge, - g3_8xlarge, - g3_16xlarge, - g4dn_xlarge, - g4dn_2xlarge, - g4dn_4xlarge, - g4dn_8xlarge, - g4dn_12xlarge, - g4dn_16xlarge, - p2_xlarge, - p2_8xlarge, - p2_16xlarge, - p3_2xlarge, - p3_8xlarge, - p3_16xlarge, - p3dn_24xlarge, - r5ad_large, - r5ad_xlarge, - r5ad_2xlarge, - r5ad_4xlarge, - r5ad_8xlarge, - r5ad_12xlarge, - r5ad_16xlarge, - r5ad_24xlarge, - r5a_large, - r5a_xlarge, - r5a_2xlarge, - r5a_4xlarge, - r5a_8xlarge, - r5a_12xlarge, - r5a_16xlarge, - r5a_24xlarge, - r5d_large, - r5d_xlarge, - r5d_2xlarge, - r5d_4xlarge, - r5d_8xlarge, - r5d_12xlarge, - r5d_16xlarge, - r5d_24xlarge, - r5d_metal, - r5_large, - r5_xlarge, - r5_2xlarge, - r5_4xlarge, - r5_8xlarge, - r5_12xlarge, - r5_16xlarge, - r5_24xlarge, - r5_metal, - r4_large, - r4_xlarge, - r4_2xlarge, - r4_4xlarge, - r4_8xlarge, - r4_16xlarge, - x1_16xlarge, - x1_32xlarge, - x1e_xlarge, - x1e_2xlarge, - x1e_4xlarge, - x1e_8xlarge, - x1e_16xlarge, - x1e_32xlarge, - z1d_large, - z1d_xlarge, - z1d_2xlarge, - z1d_3xlarge, - z1d_6xlarge, - z1d_12xlarge, - z1d_metal, - d2_xlarge, - d2_2xlarge, - d2_4xlarge, - d2_8xlarge, - i2_xlarge, - i2_2xlarge, - i2_4xlarge, - i2_8xlarge, - h1_2xlarge, - h1_4xlarge, - h1_8xlarge, - h1_16xlarge, - i3_large, - i3_xlarge, - i3_2xlarge, - i3_4xlarge, - i3_8xlarge, - i3_16xlarge, - i3_metal, - i3en_large, - i3en_xlarge, - i3en_2xlarge, - i3en_3xlarge, - i3en_6xlarge, - i3en_12xlarge, - i3en_24xlarge, - i3en_metal -} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java index 1a1c23938f4..33c2a3c5a4b 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java @@ -7,6 +7,7 @@ import com.sap.sse.landscape.application.ApplicationProcessMetrics; import com.sap.sse.landscape.application.ApplicationReplicaProcess; import com.sap.sse.landscape.application.ApplicationReplicaSet; import com.sap.sse.landscape.application.Scope; +import com.sap.sse.landscape.rabbitmq.RabbitMQEndpoint; public interface Landscape, @@ -15,4 +16,30 @@ ReplicaProcessT extends ApplicationReplicaProcess, ApplicationReplicaSet> getScopes(); + + /** + * @return the security group that shall be assigned by default to any application server host, whether master or + * replica + */ + SecurityGroup getDefaultSecurityGroupForApplicationHosts(com.sap.sse.landscape.Region region); + + /** + * @return the security group that shall be assigned by default to any host used as part of the central reverse + * proxy cluster in a region + */ + SecurityGroup getDefaultSecurityGroupForCentralReverseProxy(com.sap.sse.landscape.Region region); + + /** + * Obtains the default RabbitMQ configuration for the {@code region} specified. If nothing else is specified + * explicitly, application server replica sets launched in the {@code region} shall use this for their replication + * message channels and exchanges. + */ + RabbitMQEndpoint getMessagingConfigurationForDefaultCluster(Region region); + + /** + * Tells the regions supported. The underlying hyperscaler may have more, but we may not want to run in all. + */ + Iterable getRegions(); + + } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/MachineImage.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/MachineImage.java index 470ebb1e6f4..c638daa137b 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/MachineImage.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/MachineImage.java @@ -13,7 +13,7 @@ import com.sap.sse.common.WithID; * @author Axel Uhl (D043530) * */ -public interface MachineImage extends WithID { +public interface MachineImage extends WithID { /** * The image lives in a region; it can only be used for {@link Host} creation in that region. */ @@ -27,7 +27,7 @@ public interface MachineImage extends WithID { * * @return the new machine image that has the updated packages */ - MachineImage updateAllPackages(); + MachineImage updateAllPackages(); /** * Deletes this machine image from the infrastructure. When this method returns, this object cannot diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Process.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Process.java index 41fd5fdabf9..1b9ea75edd6 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Process.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Process.java @@ -1,5 +1,12 @@ package com.sap.sse.landscape; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.Socket; +import java.util.Optional; + +import com.sap.sse.common.Duration; + public interface Process { /** * The TCP port through which this process is typically accessed. For example, a MongoDB @@ -21,16 +28,28 @@ public interface Process { /** * Tells whether this process is still alive and will at some point (again) - * become {@link #isReady() ready} to accept requests. An example of a process {@link #isAlive() alive} - * but not {@link #isReady() ready} would be a replica process that has started to receive the initial + * become {@link #isReady(Optional) ready} to accept requests. An example of a process {@link #isAlive(Optional) alive} + * but not {@link #isReady(Optional) ready} would be a replica process that has started to receive the initial * load from its master. It can answer in a well-defined way to health check / status requests, but you - * shouldn't route regular traffic to it yet. + * shouldn't route regular traffic to it yet.

+ * + * The default implementation tries to open a socket connection to the host's public address and the {@link #getPort() port}. */ - boolean isAlive(); + default boolean isAlive(Optional optionalTimeout) throws IOException { + Socket socket = null; + try { + socket = new Socket(getHost().getPublicAddress(optionalTimeout), getPort()); + return socket.isConnected(); + } finally { + if (socket != null) { + socket.close(); + } + } + } /** * Tells whether this process is ready to accept requests. Use this for a health check in a target group - * that decides whether traffic will be sent to this process. {@link #isReady()} implies {@link #isAlive()}. + * that decides whether traffic will be sent to this process. {@link #isReady(Optional)} implies {@link #isAlive(Optional)}. */ - boolean isReady(); + boolean isReady(Optional optionalTimeout) throws MalformedURLException, IOException; } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ProcessConfigurationVariable.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ProcessConfigurationVariable.java new file mode 100644 index 00000000000..69d8dc7880a --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ProcessConfigurationVariable.java @@ -0,0 +1,134 @@ +package com.sap.sse.landscape; + +import com.jcraft.jsch.ChannelSftp; + +/** + * Fields acceptable as user data, either for launching an AWS instance with these user data which then get appended to + * an {@code env.sh} file by the {@code sailing} startup / init script, or for appending to an {@code env.sh} file + * programmatically, e.g., through a {@link ChannelSftp}. + *

+ * + * The enum literals correspond in spelling and capitalization precisely what the {@code start} script that launches a + * server process expects to find as environment variables. Therefore, the {@link Enum#name()} method can be used to + * produce those variable names. + * + * @author Axel Uhl (D043530) + * + */ +public enum ProcessConfigurationVariable { + /** + * The user data variable to use to specify the release to install and run on the host. See also + * {@link ReleaseRepository} and {@link #getReleaseUserData}. + */ + INSTALL_FROM_RELEASE, + + /** + * The user data variable used to specify the MongoDB connection URI + */ + MONGODB_URI, + + /** + * The user data variable used to define the name of the RabbitMQ exchange to which this master node + * will send its operations bound for its replica nodes. The replica-side counterpart for this is + * {@link #REPLICATE_MASTER_EXCHANGE_NAME}. + */ + REPLICATION_CHANNEL, + + /** + * Variable name for the hostname or IP address of the RabbitMQ node that this master process will use for outbound replication. + */ + REPLICATION_HOST, + + /** + * Variable name for the port used by this master process to connect to RabbitMQ for outbound replication. Using {@code 0} + * will use the default port as encoded in the RabbitMQ driver. + */ + REPLICATION_PORT, + + /** + * The user data variable used to define the server's name. This is relevant in particular for the user group + * created/used for all new server-specific objects such as the {@code SERVER} object itself. The group's + * name is constructed by appending "-server" to the server name. + */ + SERVER_NAME, + + /** + * The port on which the built-in web server of an application server process can be reached using HTTP + */ + SERVER_PORT, + + /** + * The port on which the OSGi console of a server process can be reached + */ + TELNET_PORT, + + /** + * User data variable that defines one or more comma-separated e-mail addresses to which a notification will + * be sent after the server has started successfully. + */ + SERVER_STARTUP_NOTIFY, + + /** + * User data variable defining the environment file (stored at {@code http://releases.sapsailing.com/environments}) + * which provides default combinations of variables + */ + USE_ENVIRONMENT, + + /** + * Variable for the host name or IP address where a replica can reach the master node in order to + * request the initial load, register, un-register, and send operations for reverse replication to. + * The value is always combined with that of the {@link #REPLICATE_MASTER_SERVLET_PORT} variable which + * provides the port for this communication. + */ + REPLICATE_MASTER_SERVLET_HOST, + + /** + * Variable for the port number where a replica can reach the master node in order to + * request the initial load, register, un-register, and send operations for reverse replication to. + * The value is always combined with that of the {@link #REPLICATE_MASTER_SERVLET_HOST} variable which + * provides the host name / IP address for this communication. + */ + REPLICATE_MASTER_SERVLET_PORT, + + /** + * Variable describing the name of the RabbitMQ exchange to which the master sends operations for fan-out + * distribution to all replicas, and that therefore a replica has to attach a queue to in order to receive + * those operations. Specified on a replica. The master-side counterpart is {@link #REPLICATION_CHANNEL}. + */ + REPLICATE_MASTER_EXCHANGE_NAME, + + /** + * Variable for the RabbitMQ host name that this replica will connect to in order to connect a queue to the + * fan-out exchange whose name is provided by the {@link #REPLICATE_MASTER_EXCHANGE_NAME} variable. Used + * in conjunction with the {@link #REPLICATE_MASTER_QUEUE_PORT} variable. + */ + REPLICATE_MASTER_QUEUE_HOST, + + /** + * Variable for the RabbitMQ port that this replica will connect to in order to connect a queue to the fan-out + * exchange whose name is provided by the {@link #REPLICATE_MASTER_EXCHANGE_NAME} variable. Defaults to 0 which + * instructs the driver to use the Rabbit default port (usually 5672) for connecting. Used in conjunction with the + * {@link #REPLICATE_MASTER_QUEUE_HOST} variable. + */ + REPLICATE_MASTER_QUEUE_PORT, + + /** + * Variable that specifies the IDs (basically the fully-qualified class names) of those {@code Replicable}s to + * start replicating when the server process starts. The process using this will become a replica for those + * replicables specified with this variable, and it will replicate the master node described by + * {@link #REPLICATE_MASTER_SERVLET_HOST} and {@link #REPLICATE_MASTER_SERVLET_PORT} and receive the operation + * feed through the RabbitMQ exchange configured by {@link #REPLICATE_MASTER_EXCHANGE_NAME}. + */ + REPLICATE_ON_START, + + /** + * The user data variable used to specify which bearer token to use to authenticate at the master + * in case this is to become a replica of some sort, e.g., replicating the {@code SecurityService} + * and the {@code SharedSailingData} service. + */ + REPLICATE_MASTER_BEARER_TOKEN, + + REPLICATE_MASTER_USERNAME, + + REPLICATE_MASTER_PASSWORD; +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Release.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Release.java new file mode 100644 index 00000000000..5a899f86de5 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/Release.java @@ -0,0 +1,45 @@ +package com.sap.sse.landscape; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import com.sap.sse.common.Named; +import com.sap.sse.common.TimePoint; + +/** + * Obtain from a {@link ReleaseRepository}. A release has a name that is composed of a base name and a time stamp. + * + * @author Axel Uhl (D043530) + * + */ +public interface Release extends UserDataProvider, Named { + String RELEASE_NOTES_FILE_NAME = "release-notes.txt"; + String ARCHIVE_EXTENSION = ".tar.gz"; + + ReleaseRepository getRepository(); + + String getBaseName(); + + TimePoint getCreationDate(); + + default String getFolderURL() { + return getRepository().getRepositoryBase()+"/"+getName()+"/"; + } + + default URL getReleaseNotesURL() throws MalformedURLException { + return new URL(getFolderURL()+RELEASE_NOTES_FILE_NAME); + } + + default URL getDeployableArchiveURL() throws MalformedURLException { + return new URL(getFolderURL()+getName()+ARCHIVE_EXTENSION); + } + + @Override + default Map getUserData() { + final Map result = new HashMap<>(); + result.put(ProcessConfigurationVariable.INSTALL_FROM_RELEASE, getName()); + return result; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReleaseRepository.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReleaseRepository.java new file mode 100644 index 00000000000..4221666c66d --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReleaseRepository.java @@ -0,0 +1,32 @@ +package com.sap.sse.landscape; + +/** + * Contains ready-built releases that can be used as part of the {@code INSTALL_FROM_RELEASE} directive. Iterating over + * this iterable's elements lists the {@link Release}s available. + * + * @author Axel Uhl (D043530) + * + */ +public interface ReleaseRepository extends Iterable { + /** + * @return the latest build with name prefix {@link #MASTER_RELEASE_NAME_PREFIX} if such a release exists in this + * repository, or {@code null} otherwise + */ + default Release getLatestMasterRelease() { + return getLatestRelease(getMasterReleaseNamePrefix()); + } + + /** + * Returns the latest release with the prefix specified by the parameter. + */ + Release getLatestRelease(String releaseNamePrefix); + + /** + * @return the {@link Release} with name {@code releaseName} if it exists, or {@code null} otherwise + */ + Release getRelease(String releaseName); + + String getRepositoryBase(); + + String getMasterReleaseNamePrefix(); +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReplicationConfiguration.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReplicationConfiguration.java new file mode 100644 index 00000000000..8dbf04a99c6 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReplicationConfiguration.java @@ -0,0 +1,36 @@ +package com.sap.sse.landscape; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sse.landscape.rabbitmq.RabbitMQEndpoint; + +/** + * Describes the set of replicables by their ID as well as the source to replicate them from and the credentials to use + * to authenticate the replica. The replication source is provided as a hostname and a port plus an exchange name to + * register a queue on. The hostname/port could be an IP address string identifying a single master process, or it could + * be a hostname that is mapped to a load balancer where it is forwarded to the corresponding master based on one or + * more rules and through a target group.

+ * + * This replication configuration, and in particular the {@link #getMasterExchangeName() exchange name} it is providing, + * only make sense in the context of a {@link RabbitMQEndpoint}. The {@link Landscape} + * + * @author Axel Uhl (D043530) + * + */ +public interface ReplicationConfiguration extends UserDataProvider { + String getMasterHostname(); + int getMasterHttpPort(); + String getMasterExchangeName(); + ReplicationCredentials getCredentials(); + Iterable getReplicableIds(); + default Map getUserData() { + final Map result = new HashMap<>(); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_SERVLET_HOST, getMasterHostname()); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_SERVLET_PORT, ""+getMasterHttpPort()); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_EXCHANGE_NAME, getMasterExchangeName()); + result.put(ProcessConfigurationVariable.REPLICATE_ON_START, String.join(",", getReplicableIds())); + result.putAll(getCredentials().getUserData()); + return result; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReplicationCredentials.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReplicationCredentials.java new file mode 100644 index 00000000000..4cf368ffc64 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ReplicationCredentials.java @@ -0,0 +1,13 @@ +package com.sap.sse.landscape; + +/** + * Can be used to authenticate a replica to its master. Authentication may happen by + * username/password or by a bearer token. These different types of credentials know how + * to represent themselves as environment variable settings which can, e.g., be passed as + * user data or be appended to an {@code env.sh} file directly. + * + * @author Axel Uhl (D043530) + * + */ +public interface ReplicationCredentials extends UserDataProvider { +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/UserDataProvider.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/UserDataProvider.java new file mode 100644 index 00000000000..edd04f4657c --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/UserDataProvider.java @@ -0,0 +1,18 @@ +package com.sap.sse.landscape; + +import java.util.Map; + +/** + * Configuration options can provide "user data" which can be passed to a host during + * launching or may be appended to an {@code env.sh} file that sets environment variables + * for processes to launch on a host. + * + * @see ProcessConfigurationVariable + * + * @author Axel Uhl (D043530) + * + */ +@FunctionalInterface +public interface UserDataProvider { + Map getUserData(); +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationHost.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationHost.java deleted file mode 100755 index 1f5c05c1390..00000000000 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationHost.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sap.sse.landscape.application; - -import com.sap.sse.landscape.Host; - -/** - * A host that is capable of launching {@link ApplicationProcess}es. - * - * @author Axel Uhl (D043530) - * - */ -public interface ApplicationHost extends Host { - /** - * Launches an application on this host, using the configuration parameters requested. Throws an exception - * in case the configuration is incompatible with the host, for example, if it requests a port to be used - * that is already taken. - */ - , - ReplicaProcessT extends ApplicationReplicaProcess> - ApplicationProcess launchApplication( - ApplicationProcessConfiguration configuration); -} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMachineImage.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMachineImage.java index c8437906412..f4b62e20ad7 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMachineImage.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMachineImage.java @@ -10,5 +10,5 @@ import com.sap.sse.landscape.MachineImage; * @author Axel Uhl (D043530) * */ -public interface ApplicationMachineImage extends MachineImage { +public interface ApplicationMachineImage extends MachineImage { } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMasterProcess.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMasterProcess.java index ee586ac9d30..912e503dadc 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMasterProcess.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationMasterProcess.java @@ -2,5 +2,6 @@ package com.sap.sse.landscape.application; public interface ApplicationMasterProcess, -ReplicaProcessT extends ApplicationReplicaProcess> extends ApplicationProcess { +ReplicaProcessT extends ApplicationReplicaProcess> +extends ApplicationProcess { } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java index 0655a6f1f45..c9376f1bd0a 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java @@ -1,38 +1,38 @@ package com.sap.sse.landscape.application; +import java.io.IOException; +import java.util.Optional; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; import com.sap.sse.common.Duration; import com.sap.sse.landscape.Process; +import com.sap.sse.landscape.Release; +import com.sap.sse.landscape.ReleaseRepository; import com.sap.sse.landscape.RotatingFileBasedLog; +import com.sap.sse.landscape.mongodb.Database; public interface ApplicationProcess, ReplicaProcessT extends ApplicationReplicaProcess> extends Process { /** - * @return the configuration as requested when this process was launched - */ - ApplicationProcessConfiguration getRequestedConfiguration(); - - /** - * @return the effective configuration with which this process is running, resulting from the - * {@link #getRequestedConfiguration() requested configuration} by filling in the "blanks" with defaults as - * deemed appropriate by the {@link ApplicationHost} {@link Process#getHost() running} this this process, e.g., - * to avoid any conflicting resource assignments. - */ - ApplicationProcessConfiguration getEffectiveConfiguration(); - - /** - * @return the replica set to which this process belongs + * @return the replica set to which this process belongs

+ * + * TODO define this more precisely; an instance can be replica with regards to the SecurityService and + * SharedSailingData replicables and at the same time be master for RacingEventService and all the other + * replicables. What then is "the" replica set of the process? */ ApplicationReplicaSet getReplicaSet(); - String getJavaVirtualMachineName(); - - String getJavaVirtualMachineVendor(); - - String getJavaVirtualMachineVersion(); - - JMXConnection getJMXConnection(); + /** + * @param releaseRepository + * mandatory parameter required to enable resolving the release and enabling the download of its + * artifacts, including its release notes + * @return the release that this process is currently running + */ + Release getRelease(ReleaseRepository releaseRepository, Optional optionalTimeout) throws JSchException, IOException, SftpException; /** * Tries to shut down an OSGi application server process cleanly by sending the "shutdown" OSGi command to this @@ -48,4 +48,31 @@ ReplicaProcessT extends ApplicationReplicaProcess optionalTimeout) throws NumberFormatException, JSchException, IOException, SftpException, InterruptedException; + + /** + * @return the directory as an absolute path that can be used, e.g., in a {@link ChannelSftp} to change directory to + * it or to copy files to or read files from there. + */ + String getServerDirectory(); + + /** + * The name that is the basis for the user group name; e.g., a server named "my" will by default be owned by a + * dedicated user group named "my-server". For multi-instance servers, a default setup will use this server name also + * as the base name of the {@link #getServerDirectory() server's directory}. Often, it is also used as the name of + * the {@link Database}, at least when this is a master node, and the name of the RabbitMQ fan-out exchange used + * for replication. + */ + String getServerName(Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException; + + String getEnvSh(Optional optionalTimeout) throws JSchException, IOException, SftpException; + + /** + * The URL path (everything following the hostname and starting with "/" but without any fragment) that can be + * appended to the protocol, hostname and port specification in order to produce a full health check URL. The + * health check URL, when connected to, is expected to return a 200 response if the service is healthy, and anything + * else in case it's not. + */ + String getHealthCheckPath(); } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationReplicaSet.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationReplicaSet.java index fed2fb386e9..17103c68167 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationReplicaSet.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationReplicaSet.java @@ -1,8 +1,11 @@ package com.sap.sse.landscape.application; +import java.io.IOException; import java.util.Map; +import java.util.Optional; import java.util.Set; +import com.sap.sse.common.Duration; import com.sap.sse.common.Util; import com.sap.sse.landscape.Process; @@ -26,8 +29,14 @@ ReplicaProcessT extends ApplicationReplicaProcess getReplicas(); - default Iterable getReadyReplicas() { - return Util.filter(getReplicas(), r->r.isReady()); + default Iterable getReadyReplicas(Optional optionalTimeout) { + return Util.filter(getReplicas(), r->{ + try { + return r.isReady(optionalTimeout); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); } /** @@ -67,13 +76,15 @@ ReplicaProcessT extends ApplicationReplicaProcess + * + * TODO Maybe this should move to the {@link ApplicationLoadBalancer} interface; otherwise, ApplicationReplicaSet would need to know about the load balancer(s) responsible for it * * @throws IllegalStateException * in case {@code readFromMaster} is {@code false} and there is currently no {@link #getReplicas() - * replica} currently {@link Process#isReady() ready} to receive requests. + * replica} currently {@link Process#isReady(Optional) ready} to receive requests. */ void setReadFromMaster(boolean readFromMaster) throws IllegalStateException; diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java new file mode 100644 index 00000000000..5d32dc995e1 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/impl/ApplicationProcessImpl.java @@ -0,0 +1,153 @@ +package com.sap.sse.landscape.application.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; +import com.sap.sse.common.Duration; +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.ProcessConfigurationVariable; +import com.sap.sse.landscape.Release; +import com.sap.sse.landscape.ReleaseRepository; +import com.sap.sse.landscape.RotatingFileBasedLog; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.application.ApplicationReplicaSet; +import com.sap.sse.landscape.impl.ProcessImpl; +import com.sap.sse.landscape.impl.ReleaseImpl; +import com.sap.sse.landscape.ssh.SshCommandChannel; + +public class ApplicationProcessImpl, +ReplicaProcessT extends ApplicationReplicaProcess> + extends ProcessImpl + implements ApplicationProcess { + + private static final String ENV_SH = "env.sh"; + private static final String VERSION_TXT = "configuration/jetty/version.txt"; + + /** + * Absolute path in the file system of the host on which this process is running and that represents + * this process's working directory. This directory is expected to contain a file named {@link #ENV_SH} + * whose contents can be obtained using the {@link #getEnvSh(Optional)} method. + */ + private final String serverDirectory; + + public ApplicationProcessImpl(int port, Host host, String serverDirectory) { + super(port, host); + this.serverDirectory = serverDirectory; + } + + @Override + public ApplicationReplicaSet getReplicaSet() { + // TODO Implement ApplicationProcessImpl.getReplicaSet(...) + return null; + } + + @Override + public Release getRelease(ReleaseRepository releaseRepository, Optional optionalTimeout) throws JSchException, IOException, SftpException { + final Pattern pattern = Pattern.compile("^[^-]*-([^ ]*) System:"); + final Matcher matcher = pattern.matcher(getVersionTxt(optionalTimeout)); + final Release result; + if (matcher.find()) { + result = new ReleaseImpl(matcher.group(1), releaseRepository); + } else { + result = null; + } + return result; + } + + /** + * Under the path {@code configuration/jetty/version.txt} each release is expected to present a version string in the + * format {commitId}-{releaseName} System: {mongoDbUriWithoutSchemaOrHostPortDBName}-{expeditionPort}-{replicationHost}:{replicationPort}/{replicationChannel} Started: {yyyyMMddHHmm}. + * With this it is possible to infer the release that will be run upon the next process start, which is also the + * one running now if this process is currently running and no other release has been deployed since the process + * has started. + */ + private String getVersionTxt(Optional optionalTimeout) throws JSchException, IOException, SftpException { + return getFileContents(getServerDirectory()+"/"+VERSION_TXT, optionalTimeout); + } + + @Override + public boolean tryCleanShutdown(Duration timeout, boolean forceAfterTimeout) { + // TODO Implement ApplicationProcessImpl.tryCleanShutdown(...) + return false; + } + + @Override + public int getTelnetPortToOSGiConsole(Optional optionalTimeout) throws NumberFormatException, JSchException, IOException, SftpException, InterruptedException { + return Integer.parseInt(getEnvShValueFor(getEnvSh(optionalTimeout), ProcessConfigurationVariable.TELNET_PORT, optionalTimeout)); + } + + /** + * Obtains the last definition of the process configuration variable specified, or {@code null} if that variable cannot be found + * in the {@code envShContents} string passed or that string was {@code null}. + */ + protected String getEnvShValueFor(String envShContents, String variableName, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { + final SshCommandChannel sshChannel = getHost().createRootSshChannel(optionalTimeout); + sshChannel.sendCommandLineSynchronously(". "+getEnvShPath()+">/dev/null 2>/dev/null; echo \"${"+variableName+"}\"", /* stderr */ new ByteArrayOutputStream()); + final String variableValue = sshChannel.getStreamContentsAsString(); + return variableValue.endsWith("\n") ? variableValue.substring(0, variableValue.length()-1) : variableValue; + } + + /** + * Obtains the last definition of the process configuration variable specified, or {@code null} if that variable cannot be found + * in the {@code envShContents} string passed or that string was {@code null}. + */ + protected String getEnvShValueFor(String envShContents, ProcessConfigurationVariable variable, Optional optionalTimeout) throws JSchException, IOException, InterruptedException { + return getEnvShValueFor(envShContents, variable.name(), optionalTimeout); + } + + @Override + public String getServerDirectory() { + return serverDirectory; + } + + private String getEnvShPath() { + return getServerDirectory()+"/"+ENV_SH; + } + + @Override + public String getServerName(Optional optionalTimeout) throws JSchException, IOException, InterruptedException, SftpException { + return getEnvShValueFor(getEnvSh(optionalTimeout), ProcessConfigurationVariable.SERVER_NAME, optionalTimeout); + } + + @Override + public String getEnvSh(Optional optionalTimeout) throws JSchException, IOException, SftpException { + return getFileContents(getEnvShPath(), optionalTimeout); + } + + protected String getFileContents(String path, Optional optionalTimeout) + throws JSchException, IOException, SftpException { + final ChannelSftp sftpChannel = getHost().createRootSftpChannel(optionalTimeout); + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + sftpChannel.connect((int) optionalTimeout.orElse(Duration.NULL).asMillis()); + sftpChannel.get(path, bos); + return bos.toString(); + } + + + /** + * No health check path known for arbitrary process; returning {@code null} as a default value. + */ + @Override + public String getHealthCheckPath() { + return null; + } + + /** + * Without knowledge about how to do an availability check, we report {@code true} as the default result. + */ + @Override + public boolean isReady(Optional optionalTimeout) throws MalformedURLException, IOException { + return true; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ProcessImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ProcessImpl.java new file mode 100644 index 00000000000..0ff1f1322c4 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ProcessImpl.java @@ -0,0 +1,43 @@ +package com.sap.sse.landscape.impl; + +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.Log; +import com.sap.sse.landscape.Process; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; + +public abstract class ProcessImpl implements Process { + private final int port; + private final Host host; + + public ProcessImpl(int port, Host host) { + super(); + this.port = port; + this.host = host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public Host getHost() { + return host; + } + + /** + * By default the process doesn't have a log. + */ + @Override + public LogT getLog() { + return null; + } + + /** + * By default, the process doesn't expose any metrics. + */ + @Override + public MetricsT getMetrics() { + return null; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ReleaseImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ReleaseImpl.java new file mode 100644 index 00000000000..cf28291ddc6 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ReleaseImpl.java @@ -0,0 +1,47 @@ +package com.sap.sse.landscape.impl; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.NamedImpl; +import com.sap.sse.landscape.Release; +import com.sap.sse.landscape.ReleaseRepository; + +public class ReleaseImpl extends NamedImpl implements Release { + private static final Logger logger = Logger.getLogger(ReleaseImpl.class.getName()); + private static final long serialVersionUID = -225240683033821028L; + + private final ReleaseRepository repository; + + public ReleaseImpl(String name, ReleaseRepository repository) { + super(name); + this.repository = repository; + } + + @Override + public ReleaseRepository getRepository() { + return repository; + } + + @Override + public String getBaseName() { + return getName().substring(0, getName().lastIndexOf("-")); + } + + @Override + public TimePoint getCreationDate() { + final String dateSubstring = getName().substring(getName().lastIndexOf("-")+1); + try { + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmm"); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return TimePoint.of(simpleDateFormat.parse(dateSubstring)); + } catch (ParseException e) { + logger.log(Level.WARNING, "Error parsing release date "+dateSubstring+". Returning null instead.", e); + return null; + } + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ReleaseRepositoryImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ReleaseRepositoryImpl.java new file mode 100644 index 00000000000..b84c0532738 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/impl/ReleaseRepositoryImpl.java @@ -0,0 +1,89 @@ +package com.sap.sse.landscape.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.sap.sse.common.Util; +import com.sap.sse.landscape.Release; +import com.sap.sse.landscape.ReleaseRepository; + +public class ReleaseRepositoryImpl implements ReleaseRepository { + private static final Logger logger = Logger.getLogger(ReleaseRepositoryImpl.class.getName()); + private final String repositoryBase; + + private final String masterReleaseNamePrefix; + + public ReleaseRepositoryImpl(String repositoryBase, String masterReleaseNamePrefix) { + super(); + this.repositoryBase = repositoryBase; + this.masterReleaseNamePrefix = masterReleaseNamePrefix; + } + + @Override + public String getRepositoryBase() { + return repositoryBase; + } + + @Override + public String getMasterReleaseNamePrefix() { + return masterReleaseNamePrefix; + } + + private Iterable getAvailableReleases() { + final List result = new LinkedList<>(); + try { + final InputStream index = (InputStream) new URL(getRepositoryBase()).getContent(); + int read = 0; + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + while ((read=index.read()) != -1) { + bos.write(read); + } + index.close(); + final String contents = bos.toString(); + final Pattern pattern = Pattern.compile("([^/]*)-([0-9]*)/"); + final Matcher m = pattern.matcher(contents); + int lastMatch = 0; + while (m.find(lastMatch)) { + result.add(new ReleaseImpl(m.group(1), this)); + lastMatch = m.end(); + } + } catch (IOException e) { + logger.log(Level.SEVERE, "Error loading releases from repository at "+getRepositoryBase()+". Continuing empty.", e); + return Collections.emptyList(); + } + return result; + } + + @Override + public Iterator iterator() { + return getAvailableReleases().iterator(); + } + + @Override + public Release getLatestRelease(String releaseNamePrefix) { + Release result = null; + for (final Release release : getAvailableReleases()) { + if (release.getBaseName().equals(releaseNamePrefix) && + (result == null || release.getCreationDate().after(result.getCreationDate()))) { + result = release; + } + } + return result; + } + + @Override + public Release getRelease(String releaseName) { + return Util.first(Util.filter(getAvailableReleases(), r->r.getName().equals(releaseName))); + } + +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/Database.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/Database.java index 8e50a64c013..67657ab9d10 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/Database.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/Database.java @@ -1,27 +1,39 @@ package com.sap.sse.landscape.mongodb; import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import com.mongodb.client.MongoDatabase; import com.sap.sse.common.Named; +import com.sap.sse.landscape.ProcessConfigurationVariable; +import com.sap.sse.landscape.UserDataProvider; -public interface Database extends Named { +public interface Database extends UserDataProvider, Named { /** * Obtains the database-specific connection URI that clients can use to connect to the replica set hosting this * database. See also {@link MongoReplicaSet#getConnectionURI()}. */ - URI getConnectionURI(); + default URI getConnectionURI() throws URISyntaxException { + return getEndpoint().getURI(Optional.of(this)); + } - MongoReplicaSet getReplicaSet(); + MongoEndpoint getEndpoint(); - /** - * @return the collections currently stored in this database - */ - Iterable getCollections(); - - String getMD5Hash(); - - /** - * Drops this database with all its collections and all their contents. Use with care! - */ - void drop(); + default MongoDatabase getMongoDatabase() throws URISyntaxException { + return getEndpoint().getMongoDatabase(getName()); + } + + @Override + default Map getUserData() { + final Map result = new HashMap<>(); + try { + result.put(ProcessConfigurationVariable.MONGODB_URI, getConnectionURI().toString()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + return result; + } } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoEndpoint.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoEndpoint.java new file mode 100644 index 00000000000..cb9fd33d0e0 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoEndpoint.java @@ -0,0 +1,48 @@ +package com.sap.sse.landscape.mongodb; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +import com.mongodb.MongoClient; +import com.mongodb.MongoClientURI; +import com.mongodb.client.MongoDatabase; +import com.mongodb.session.ClientSession; + +/** + * A MongoDB endpoint that an application can connect to. It can produce a {@link URI} the client can use to connect to, + * e.g., with a {@code MongoClientURI}. The endpoint can be a standalong MongoDB instance, represented by a single + * {@link MongoProcess}, or it may be a {@link MongoReplicaSet replica set}. + * + * @author Axel Uhl (D043530) + * + */ +public interface MongoEndpoint { + URI getURI(Optional optionalDb) throws URISyntaxException; + + Iterable getMongoDatabases() throws URISyntaxException; + + MongoDatabase getMongoDatabase(String dbName) throws URISyntaxException; + + /** + * Imports a {@link Database} from another {@link MongoReplicaSet} which must be different from {@code this} + * replica set (otherwise an {@link IllegalArgumentException} is thrown). Before dropping / removing the + * {@code from} database, callers may want to compare the {@link Database#getMD5Hash() hash} of {@code from} + * with that of the {@link Database} returned by this method. + */ + MongoDatabase importDatabase(MongoDatabase from); + + boolean isInReplicaSet() throws URISyntaxException; + + MongoClientURI getMongoClientURI(Optional optionalDb) throws URISyntaxException; + + MongoClient getClient() throws URISyntaxException; + + ClientSession getClientSession() throws URISyntaxException; + + default String getMD5Hash(String databaseName) throws URISyntaxException { + return getMD5Hash(getMongoDatabase(databaseName)); + } + + String getMD5Hash(MongoDatabase database) throws URISyntaxException; +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcess.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcess.java index 66b7c6aa652..a94f6cf2d71 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcess.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcess.java @@ -1,11 +1,33 @@ package com.sap.sse.landscape.mongodb; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + import com.sap.sse.landscape.Process; import com.sap.sse.landscape.RotatingFileBasedLog; -public interface MongoProcess extends Process { +public interface MongoProcess extends Process, MongoEndpoint { int DEFAULT_PORT = 27017; - boolean isHidden(); - int getPriority(); - int getVotes(); + + @Override + default URI getURI(Optional optionalDb) throws URISyntaxException { + final StringBuilder sb = new StringBuilder(); + sb.append("mongodb://"); + sb.append(getHost().getPublicAddress().getCanonicalHostName()); + sb.append(":"); + sb.append(getPort()); + sb.append("/"); + optionalDb.ifPresent(db->sb.append(db.getName())); + return new URI(sb.toString()); + } + + boolean isInReplicaSet() throws URISyntaxException; + + /** + * If not {@link #isInReplicaSet()} then a {@link ClassCastException} will be thrown. + */ + default MongoProcessInReplicaSet asMongoProcessInReplicaSet() { + return (MongoProcessInReplicaSet) this; + } } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcessInReplicaSet.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcessInReplicaSet.java new file mode 100644 index 00000000000..6fdcce27102 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoProcessInReplicaSet.java @@ -0,0 +1,4 @@ +package com.sap.sse.landscape.mongodb; + +public interface MongoProcessInReplicaSet extends MongoProcess { +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoReplicaSet.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoReplicaSet.java index affe5b6150b..e2159613ff4 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoReplicaSet.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/MongoReplicaSet.java @@ -8,14 +8,15 @@ import java.util.Optional; import com.sap.sse.common.Named; -public interface MongoReplicaSet extends Named { +public interface MongoReplicaSet extends Named, MongoEndpoint { Iterable getInstances(); /** * The {@code "mongodb://..."} URI that application use to connect to this replica set; not specific * to any particular database managed by this replica set; see also {@link Database#getConnectionURI()}. */ - default URI getConnectionURI(Optional optionalDb) throws URISyntaxException { + @Override + default URI getURI(Optional optionalDb) throws URISyntaxException { final StringBuilder result = new StringBuilder("mongodb://"); final List hostSpecs = new ArrayList<>(); for (final MongoProcess mongoProcess : getInstances()) { @@ -32,23 +33,11 @@ public interface MongoReplicaSet extends Named { optionalDb.ifPresent(db->result.append(db.getName())); result.append("?replicaSet="); result.append(getName()); - result.append("&retryWrites=true"); + result.append("&retryWrites=true&readPreference=nearest"); return new URI(result.toString()); } void addReplica(MongoProcess newReplica); void removeReplica(MongoProcess replicaToRemove); - - Iterable getDatabases(); - - Database getDatabase(String dbName); - - /** - * Imports a {@link Database} from another {@link MongoReplicaSet} which must be different from {@code this} - * replica set (otherwise an {@link IllegalArgumentException} is thrown). Before dropping / removing the - * {@code from} database, callers may want to compare the {@link Database#getMD5Hash() hash} of {@code from} - * with that of the {@link Database} returned by this method. - */ - Database importDatabase(Database from); } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/DatabaseImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/DatabaseImpl.java new file mode 100644 index 00000000000..c3b6c9d8c8b --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/DatabaseImpl.java @@ -0,0 +1,20 @@ +package com.sap.sse.landscape.mongodb.impl; + +import com.sap.sse.common.impl.NamedImpl; +import com.sap.sse.landscape.mongodb.Database; +import com.sap.sse.landscape.mongodb.MongoEndpoint; + +public class DatabaseImpl extends NamedImpl implements Database { + private static final long serialVersionUID = -3816220771822620058L; + private final MongoEndpoint endpoint; + + public DatabaseImpl(MongoEndpoint endpoint, String name) { + super(name); + this.endpoint = endpoint; + } + + @Override + public MongoEndpoint getEndpoint() { + return endpoint; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoEndpointImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoEndpointImpl.java new file mode 100644 index 00000000000..650ec7eb0ad --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoEndpointImpl.java @@ -0,0 +1,60 @@ +package com.sap.sse.landscape.mongodb.impl; + +import java.net.URISyntaxException; +import java.util.Optional; + +import org.bson.Document; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientURI; +import com.mongodb.client.MongoDatabase; +import com.mongodb.session.ClientSession; +import com.sap.sse.common.Util; +import com.sap.sse.landscape.mongodb.Database; +import com.sap.sse.landscape.mongodb.MongoEndpoint; + +public abstract class MongoEndpointImpl implements MongoEndpoint { + @Override + public Iterable getMongoDatabases() throws URISyntaxException { + final MongoClient client = getClient(); + return Util.map(client.listDatabaseNames(), dbName->client.getDatabase(dbName)); + } + + @Override + public MongoDatabase getMongoDatabase(String dbName) throws URISyntaxException { + return getClient().getDatabase(dbName); + } + + @Override + public MongoDatabase importDatabase(MongoDatabase from) { + // TODO Implement MongoReplicaSetImpl.importDatabase(...) + return null; + } + + @Override + public boolean isInReplicaSet() throws URISyntaxException { + return getClient().getReplicaSetStatus() != null; + } + + @Override + public MongoClientURI getMongoClientURI(Optional optionalDb) throws URISyntaxException { + return new MongoClientURI(getURI(optionalDb).toString()); + } + + @Override + public MongoClient getClient() throws URISyntaxException { + return new MongoClient(getMongoClientURI(Optional.empty())); + } + + @Override + public ClientSession getClientSession() throws URISyntaxException { + return getClient().startSession(ClientSessionOptions.builder().causallyConsistent(true).build()); + } + + + @Override + public String getMD5Hash(MongoDatabase database) throws URISyntaxException { + return database.runCommand(new Document("dbHash", 1)).get("md5").toString(); + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoProcessImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoProcessImpl.java new file mode 100644 index 00000000000..77a8ec75782 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoProcessImpl.java @@ -0,0 +1,63 @@ +package com.sap.sse.landscape.mongodb.impl; + +import java.net.URISyntaxException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sse.common.Duration; +import com.sap.sse.common.Util; +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.RotatingFileBasedLog; +import com.sap.sse.landscape.mongodb.MongoMetrics; +import com.sap.sse.landscape.mongodb.MongoProcess; + +public class MongoProcessImpl extends MongoEndpointImpl implements MongoProcess { + private static final String LOCAL_DB_NAME = "local"; + private static final Logger logger = Logger.getLogger(MongoProcessImpl.class.getName()); + private final int port; + private final Host host; + + public MongoProcessImpl(Host host) { + this(DEFAULT_PORT, host); + } + + public MongoProcessImpl(int port, Host host) { + super(); + this.port = port; + this.host = host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public Host getHost() { + return host; + } + + @Override + public RotatingFileBasedLog getLog() { + // TODO Implement MongoProcessImpl.getLog(...) + return null; + } + + @Override + public MongoMetrics getMetrics() { + // TODO Implement MongoProcessImpl.getMetrics(...) + return null; + } + + @Override + public boolean isReady(Optional optionalTimeout) { + try { + return Util.contains(getClient().listDatabaseNames(), LOCAL_DB_NAME); + } catch (URISyntaxException e) { + logger.log(Level.SEVERE, "Internal error constructing MongoDB client URI", e); + throw new RuntimeException(e); + } + } + +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoProcessInReplicaSetImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoProcessInReplicaSetImpl.java new file mode 100644 index 00000000000..90e2c6c1bbc --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoProcessInReplicaSetImpl.java @@ -0,0 +1,31 @@ +package com.sap.sse.landscape.mongodb.impl; + +import java.net.URISyntaxException; + +import com.mongodb.MongoClient; +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.mongodb.MongoProcessInReplicaSet; +import com.sap.sse.landscape.mongodb.MongoReplicaSet; + +public class MongoProcessInReplicaSetImpl extends MongoProcessImpl implements MongoProcessInReplicaSet { + private final MongoReplicaSet replicaSet; + + public MongoProcessInReplicaSetImpl(MongoReplicaSet replicaSet, Host host) { + super(host); + this.replicaSet = replicaSet; + } + + public MongoProcessInReplicaSetImpl(MongoReplicaSet replicaSet, int port, Host host) { + super(port, host); + this.replicaSet = replicaSet; + } + + @Override + public boolean isInReplicaSet() throws URISyntaxException { + return getReplicaSetClient().getReplicaSetStatus() != null; + } + + private MongoClient getReplicaSetClient() throws URISyntaxException { + return replicaSet.getClient(); + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoReplicaSetImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoReplicaSetImpl.java new file mode 100644 index 00000000000..5ccfe1a7740 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/mongodb/impl/MongoReplicaSetImpl.java @@ -0,0 +1,40 @@ +package com.sap.sse.landscape.mongodb.impl; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.sap.sse.landscape.mongodb.MongoProcess; +import com.sap.sse.landscape.mongodb.MongoReplicaSet; + +public class MongoReplicaSetImpl extends MongoEndpointImpl implements MongoReplicaSet { + private static final long serialVersionUID = 2055195945588247106L; + private final String name; + private final Set instances; + + public MongoReplicaSetImpl(String name) { + super(); + this.name = name; + this.instances = new HashSet<>(); + } + + @Override + public String getName() { + return name; + } + + @Override + public Iterable getInstances() { + return Collections.unmodifiableSet(instances); + } + + @Override + public void addReplica(MongoProcess newReplica) { + instances.add(newReplica); + } + + @Override + public void removeReplica(MongoProcess replicaToRemove) { + instances.remove(replicaToRemove); + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/AbstractProcedureImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/AbstractProcedureImpl.java new file mode 100644 index 00000000000..52b3fc597dd --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/AbstractProcedureImpl.java @@ -0,0 +1,24 @@ +package com.sap.sse.landscape.orchestration; + +import com.sap.sse.landscape.Landscape; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; + +public abstract class AbstractProcedureImpl, +ReplicaProcessT extends ApplicationReplicaProcess> implements Procedure { + private final Landscape landscape; + + public AbstractProcedureImpl(Landscape landscape) { + super(); + this.landscape = landscape; + } + + @Override + public Landscape getLandscape() { + return landscape; + } + +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/Procedure.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/Procedure.java new file mode 100644 index 00000000000..1ce7e53b0f3 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/Procedure.java @@ -0,0 +1,57 @@ +package com.sap.sse.landscape.orchestration; + +import java.util.Arrays; + +import com.sap.sse.concurrent.RunnableWithException; +import com.sap.sse.landscape.Landscape; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.orchestration.impl.ProcedureSequence; + +/** + * Encodes a potentially compound sequence of actions that transform the landscape from one state to another, trying to + * achieve a certain goal. For example, there may be a procedure for setting up a dedicated master application server + * which is a component of another procedure which sets up an application server replica set which in turn is part of a + * procedure for not only setting the replica set up but also wiring it with an "Ingress" in a combination of DNS + * records, load balancer rules, target groups, necessary assignments of the instances to target groups, as well as + * reverse proxy configurations on each application server replica set node. + *

+ * + * Other procedures may set up a MongoDB replica set which refers to a procedure for setting up a single MongoDB + * instance, or for archiving an event, or for scaling out an application server replica set by adding more replicas to + * it. + *

+ * + * A procedure may succeed or it may fail. Ideally, procedures support reverting their actions. This way, if a composite + * procedure fails half way through, sub-procedures may be reverted to leave the landscape again in a consistent state. + *

+ * + * Procedures should log their actions verbosely and in the log messages clearly tell which steps were taken and whether + * or not they succeeded. + *

+ * + * A procedure may be parameterizable. For example, a procedure for setting up a dedicated application server replica + * set for an event could offer as its parameters an event-specific hostname / sub-domain name to be used, as well as a + * name for a corresponding MongoDB database, or even offer to infer all that from a single event name such as "kw2020". + * Further typical and common parameters for a procedure could be landscape-oriented specifications such as the region + * or availability zone to which to deploy something. + * + * @author Axel Uhl (D043530) + * + */ +public interface Procedure, + ReplicaProcessT extends ApplicationReplicaProcess> +extends RunnableWithException { + Landscape getLandscape(); + + default Procedure andThen(Procedure nextStep) { + return new ProcedureSequence<>(getLandscape(), Arrays.asList(this, nextStep)); + } + + default Procedure fromSteps(Iterable> steps) { + return new ProcedureSequence<>(getLandscape(), steps); + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/StartHost.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/StartHost.java new file mode 100644 index 00000000000..975b66964a8 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/StartHost.java @@ -0,0 +1,45 @@ +package com.sap.sse.landscape.orchestration; + +import com.sap.sse.landscape.Host; +import com.sap.sse.landscape.Landscape; +import com.sap.sse.landscape.MachineImage; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; + +public abstract class StartHost, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends Host> +extends AbstractProcedureImpl +implements Procedure { + private final MachineImage machineImage; + + /** + * A builder that helps building an instance of type {@link StartHost} or any subclass thereof (then using + * specialized builders). + * + * @author Axel Uhl (D043530) + */ + public static interface Builder, ShardingKey, + MetricsT extends ApplicationProcessMetrics, + MasterProcessT extends ApplicationMasterProcess, + ReplicaProcessT extends ApplicationReplicaProcess, + HostT extends Host> { + T build(); + + MachineImage getMachineImage(); + + Landscape getLandscape(); + } + + protected StartHost(Builder, ShardingKey, MetricsT, MasterProcessT, ReplicaProcessT, HostT> builder) { + super(builder.getLandscape()); + this.machineImage = builder.getMachineImage(); + } + + protected MachineImage getMachineImage() { + return machineImage; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/impl/ProcedureSequence.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/impl/ProcedureSequence.java new file mode 100644 index 00000000000..2c5e9597bc7 --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/orchestration/impl/ProcedureSequence.java @@ -0,0 +1,37 @@ +package com.sap.sse.landscape.orchestration.impl; + +import java.util.Iterator; + +import com.sap.sse.landscape.Landscape; +import com.sap.sse.landscape.application.ApplicationMasterProcess; +import com.sap.sse.landscape.application.ApplicationProcessMetrics; +import com.sap.sse.landscape.application.ApplicationReplicaProcess; +import com.sap.sse.landscape.orchestration.AbstractProcedureImpl; +import com.sap.sse.landscape.orchestration.Procedure; + +public class ProcedureSequence, +ReplicaProcessT extends ApplicationReplicaProcess> +extends AbstractProcedureImpl +implements Procedure, Iterable> { + private final Iterable> steps; + + public ProcedureSequence(Landscape landscape, + Iterable> steps) { + super(landscape); + this.steps = steps; + } + + @Override + public void run() throws Exception { + for (final Procedure s : this) { + s.run(); + } + } + + @Override + public Iterator> iterator() { + return steps.iterator(); + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQEndpoint.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQEndpoint.java new file mode 100644 index 00000000000..605584646fc --- /dev/null +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQEndpoint.java @@ -0,0 +1,41 @@ +package com.sap.sse.landscape.rabbitmq; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sse.landscape.ProcessConfigurationVariable; +import com.sap.sse.landscape.UserDataProvider; + +/** + * Shall allow a client to connect to a RabbitMQ service which may or may not be replicated. For now, we assume that the + * connectivity information required consists of a port and a hostname which in Rabbit / Erlang terminology may be + * called a "node name." + *

+ * + * The interface can be implemented easily by a lambda providing the {@link #getNodeName() node name}, such as + * {@code rabbit.internal.sapsailing.com}. + * + * @author Axel Uhl (D043530) + * + */ +@FunctionalInterface +public interface RabbitMQEndpoint extends UserDataProvider { + int DEFAULT_PORT = 5672; + + default int getPort() { + return DEFAULT_PORT; + } + + String getNodeName(); + + /** + * Renders the RabbitMQ configuration as a set of variable assignments usable in either an {@code env.sh} file or in the + * AWS EC2 instance user data (which eventually get appended to an {@code env.sh} file). + */ + default Map getUserData() { + final Map result = new HashMap<>(); + result.put(ProcessConfigurationVariable.REPLICATION_HOST, getNodeName()); + result.put(ProcessConfigurationVariable.REPLICATE_MASTER_QUEUE_PORT, ""+getPort()); + return result; + } +} diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQInstance.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQInstance.java index 54cd9de1e34..85efe82d742 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQInstance.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQInstance.java @@ -3,6 +3,8 @@ package com.sap.sse.landscape.rabbitmq; import com.sap.sse.landscape.Process; import com.sap.sse.landscape.RotatingFileBasedLog; -public interface RabbitMQInstance extends Process { - +public interface RabbitMQInstance extends Process, RabbitMQEndpoint { + default int getPort() { + return DEFAULT_PORT; + } } diff --git a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQReplicaSet.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQReplicaSet.java index 12efa594580..9b6fa2a05e9 100755 --- a/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQReplicaSet.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/rabbitmq/RabbitMQReplicaSet.java @@ -1,6 +1,6 @@ package com.sap.sse.landscape.rabbitmq; -public interface RabbitMQReplicaSet { +public interface RabbitMQReplicaSet extends RabbitMQEndpoint { Iterable getInstances(); Iterable getExchanges(); diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/SshCommandChannel.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannel.java old mode 100755 new mode 100644 similarity index 81% rename from java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/SshCommandChannel.java rename to java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannel.java index a0bbe6625d3..79d7f8b027c --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/SshCommandChannel.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannel.java @@ -1,4 +1,4 @@ -package com.sap.sse.landscape.aws; +package com.sap.sse.landscape.ssh; import java.io.IOException; import java.io.InputStream; @@ -32,4 +32,10 @@ public interface SshCommandChannel { * disconnects and closes the channel. */ byte[] getStreamContentsAsByteArray() throws IOException, JSchException; + + /** + * Consumes the entire input stream content as provided by {@link #sendCommandLineSynchronously(String, OutputStream)} and then + * disconnects and closes the channel. + */ + String getStreamContentsAsString() throws IOException, JSchException; } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/SshCommandChannelImpl.java b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannelImpl.java old mode 100755 new mode 100644 similarity index 85% rename from java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/SshCommandChannelImpl.java rename to java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannelImpl.java index c3d0c302e3b..7de3ce0ff25 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/SshCommandChannelImpl.java +++ b/java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannelImpl.java @@ -1,4 +1,4 @@ -package com.sap.sse.landscape.aws.impl; +package com.sap.sse.landscape.ssh; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -10,7 +10,6 @@ import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; -import com.sap.sse.landscape.aws.SshCommandChannel; public class SshCommandChannelImpl implements SshCommandChannel { @@ -43,6 +42,17 @@ public class SshCommandChannelImpl implements SshCommandChannel { @Override public byte[] getStreamContentsAsByteArray() throws IOException, JSchException { + final ByteArrayOutputStream bos = readStdout(); + return bos.toByteArray(); + } + + @Override + public String getStreamContentsAsString() throws IOException, JSchException { + final ByteArrayOutputStream bos = readStdout(); + return bos.toString(); + } + + private ByteArrayOutputStream readStdout() throws IOException, JSchException { final ByteArrayOutputStream bos = new ByteArrayOutputStream(); final byte[] buf = new byte[8192]; int read; @@ -58,7 +68,7 @@ public class SshCommandChannelImpl implements SshCommandChannel { } } channel.disconnect(); - return bos.toByteArray(); + return bos; } private String getHost() throws JSchException { diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java index 1d70fdba3a5..d7c1ed59361 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java @@ -220,7 +220,7 @@ public class RoleDefinitionsPanel extends VerticalPanel { userService.getUserManagementWriteService(), type, roleDefinition -> updateRoleDefinitions(), stringMessages); roleActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - configACL::openACLDialog); + configACL::openDialog); table.addColumn(roleSelectionCheckboxColumn, roleSelectionCheckboxColumn.getHeader()); table.addColumn(roleDefinitionNameColumn, stringMessages.name()); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupTableWrapper.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupTableWrapper.java index 91949a867cf..a3211b83adf 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupTableWrapper.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupTableWrapper.java @@ -102,7 +102,7 @@ public class UserGroupTableWrapper extends actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_OWNERSHIP, DefaultActions.CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); actionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - u -> configACL.openACLDialog(u)); + u -> configACL.openDialog(u)); filterField = new LabeledAbstractFilterablePanel(new Label(stringMessages.filterUserGroups()), new ArrayList(), dataProvider, stringMessages) { diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/editacl/EditACLDialog.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/editacl/EditACLDialog.java index 01366a2921a..d662963c88b 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/editacl/EditACLDialog.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/editacl/EditACLDialog.java @@ -84,7 +84,7 @@ public class EditACLDialog extends DataEntryDialog { /** * Creates a new {@link DialogConfig dialog configuration} instance which can be (re-)used to - * {@link DialogConfig#openACLDialog(Named) open} a {@link EditACLDialog dialog}. + * {@link DialogConfig#openDialog(Named) open} a {@link EditACLDialog dialog}. * * @param userManagementWriteService * {@link UserManagementServiceAsync} to use to set the secured object's ownership @@ -127,7 +127,7 @@ public class EditACLDialog extends DataEntryDialog { * @param securedObject * {@link Named} {@link SecuredObject} instance to edit ownerships for */ - public void openACLDialog(final T securedObject) { + public void openDialog(final T securedObject) { new EditACLDialog(userManagementWriteService, identifierFactory.apply(securedObject), availableActionsFactory.get(), stringMessages, new EditAclDialogCallback(securedObject)).show(); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java index 47a63a468ef..9a23f6909d1 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java @@ -190,7 +190,7 @@ extends TableWrapper { user -> user.getAccessControlList(), stringMessages); userActionColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); userActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - u -> configACL.openACLDialog(u)); + u -> configACL.openDialog(u)); filterField = new LabeledAbstractFilterablePanel(new Label(stringMessages.filterUsers()), new ArrayList(), dataProvider, stringMessages) { @Override diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/permissions/WildcardPermissionWithSecurityDTOTableWrapper.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/permissions/WildcardPermissionWithSecurityDTOTableWrapper.java index 87ce9373fb3..af48c7e0f23 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/permissions/WildcardPermissionWithSecurityDTOTableWrapper.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/permissions/WildcardPermissionWithSecurityDTOTableWrapper.java @@ -107,7 +107,7 @@ public class WildcardPermissionWithSecurityDTOTableWrapper extends userService.getUserManagementWriteService(), type, user -> user.getAccessControlList(), stringMessages); userActionColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); userActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - permission -> configACL.openACLDialog(permission)); + permission -> configACL.openDialog(permission)); // filter field configuration filterField = new LabeledAbstractFilterablePanel( new Label(stringMessages.filterPermission()), new ArrayList(), diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/RoleWithSecurityDTOTableWrapper.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/RoleWithSecurityDTOTableWrapper.java index 4b6aafe7955..2f84d3f9ed6 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/RoleWithSecurityDTOTableWrapper.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/RoleWithSecurityDTOTableWrapper.java @@ -109,7 +109,7 @@ public class RoleWithSecurityDTOTableWrapper extends userService.getUserManagementWriteService(), type, user -> user.getAccessControlList(), stringMessages); userActionColumn.addAction(ACTION_CHANGE_OWNERSHIP, CHANGE_OWNERSHIP, configOwnership::openOwnershipDialog); userActionColumn.addAction(DefaultActionsImagesBarCell.ACTION_CHANGE_ACL, DefaultActions.CHANGE_ACL, - permission -> configACL.openACLDialog(permission)); + permission -> configACL.openDialog(permission)); // filter field configuration filterField = new LabeledAbstractFilterablePanel(new Label(stringMessages.filterRoles()), new ArrayList(), dataProvider, stringMessages) { diff --git a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/AccessControlStoreImpl.java b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/AccessControlStoreImpl.java index 84a258fd8ed..11da29217a3 100644 --- a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/AccessControlStoreImpl.java +++ b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/AccessControlStoreImpl.java @@ -10,8 +10,8 @@ import java.util.concurrent.ConcurrentHashMap; import com.sap.sse.common.Util; import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.LockUtil.RunnableWithResult; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.concurrent.RunnableWithResult; import com.sap.sse.security.interfaces.AccessControlStore; import com.sap.sse.security.interfaces.UserStore; import com.sap.sse.security.shared.AccessControlListAnnotation; diff --git a/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/LockUtil.java b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/LockUtil.java index 7093831744b..1f975a6c6a5 100644 --- a/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/LockUtil.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/LockUtil.java @@ -50,21 +50,6 @@ import com.sap.sse.util.impl.ApproximateTime; public class LockUtil { private enum ReadOrWrite { READ, WRITE }; - /** - * Can be replaced with java.util.function.Supplier when we can consistently use Java 8. - */ - public interface RunnableWithResult { - T run(); - } - - public interface RunnableWithResultAndException { - T run() throws E; - } - - public interface RunnableWithException { - void run() throws E; - } - private static final int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5; private static final Logger logger = Logger.getLogger(Util.class.getName()); private static final Map lastTimeWriteLockWasObtained = new ConcurrentWeakHashMap(); diff --git a/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithException.java b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithException.java new file mode 100644 index 00000000000..bec41516445 --- /dev/null +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithException.java @@ -0,0 +1,6 @@ +package com.sap.sse.concurrent; + +@FunctionalInterface +public interface RunnableWithException { + void run() throws E; +} \ No newline at end of file diff --git a/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithResult.java b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithResult.java new file mode 100644 index 00000000000..205972aa10b --- /dev/null +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithResult.java @@ -0,0 +1,9 @@ +package com.sap.sse.concurrent; + +/** + * Can be replaced with java.util.function.Supplier when we can consistently use Java 8. + */ +@FunctionalInterface +public interface RunnableWithResult { + T run(); +} \ No newline at end of file diff --git a/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithResultAndException.java b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithResultAndException.java new file mode 100644 index 00000000000..0462094fe0b --- /dev/null +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/concurrent/RunnableWithResultAndException.java @@ -0,0 +1,6 @@ +package com.sap.sse.concurrent; + +@FunctionalInterface +public interface RunnableWithResultAndException { + T run() throws E; +} \ No newline at end of file diff --git a/java/com.sap.sse.test/src/com/sap/sse/test/VariableArityParameterNeverNullTest.java b/java/com.sap.sse.test/src/com/sap/sse/test/VariableArityParameterNeverNullTest.java new file mode 100644 index 00000000000..1eea669304f --- /dev/null +++ b/java/com.sap.sse.test/src/com/sap/sse/test/VariableArityParameterNeverNullTest.java @@ -0,0 +1,16 @@ +package com.sap.sse.test; + +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class VariableArityParameterNeverNullTest { + @Test + public void callVariableArityWithoutActualValue() { + m(); + } + + private void m(String... variableArityFormalParam) { + assertNotNull(variableArityFormalParam); + } +} diff --git a/java/org.mp4parser.isoparser/META-INF/MANIFEST.MF b/java/org.mp4parser.isoparser/META-INF/MANIFEST.MF index 11deee0d099..50e99733f0c 100644 --- a/java/org.mp4parser.isoparser/META-INF/MANIFEST.MF +++ b/java/org.mp4parser.isoparser/META-INF/MANIFEST.MF @@ -7,6 +7,7 @@ Bundle-ClassPath: ., lib/isoparser-1.9.37.jar Bundle-Vendor: Sebastian Annies Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy Export-Package: com.sap.sailing.media.mp4, org.mp4parser, org.mp4parser.boxes, diff --git a/java/org.mp4parser.isoparser/build.properties b/java/org.mp4parser.isoparser/build.properties index 11130baf9b3..23ee79f8f3f 100644 --- a/java/org.mp4parser.isoparser/build.properties +++ b/java/org.mp4parser.isoparser/build.properties @@ -1,5 +1,6 @@ bin.includes = META-INF/,\ - lib/isoparser-1.9.37.jar + lib/isoparser-1.9.37.jar,\ + . jars.compile.order = . source.. = src/ output.. = bin/ diff --git a/java/pom.xml b/java/pom.xml index fd1ed403088..26ca03d2b9e 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -84,6 +84,7 @@ com.sap.sse.landscape.aws.test com.sap.sse.landscape.aws.persistence com.sap.sailing.landscape + com.sap.sailing.landscape.test com.sap.sse.jersey.jaxbdependencyfragment com.sap.sse.jettyextensions com.sap.sailing.routeconverterjava11extension diff --git a/java/target/start b/java/target/start index b7c428841f9..bae370ec0b7 100755 --- a/java/target/start +++ b/java/target/start @@ -69,8 +69,12 @@ sed $SED_ARGS "s/127.0.0.1:[0-9][0-9][0-9][0-9]\//127.0.0.1:$SERVER_PORT\//g" ./ # Inject information for system HEAD_DATE=$(date "+%Y%m%d%H%M") -MONGODB_URI_FOR_SED=$( echo "$MONGODB_URI" | sed -e 's/^mongodb:\/\///' -e 's/^[^:]*:[^@]*@//' -e 's/\//\\\//g' -e 's/&/\\&/g' ) -sed $SED_ARGS "s/System:.*$/ System: $MONGODB_URI_FOR_SED:$MONGODB_HOST:$MONGODB_PORT\/$MONGODB_NAME-$EXPEDITION_PORT-$REPLICATION_HOST:$REPLICATION_PORT\/$REPLICATION_CHANNEL Started: $HEAD_DATE/g" ./configuration/jetty/version.txt +if [ "${MONGODB_URI}" != "" ]; then + MONGODB_URI_FOR_SED=$( echo "$MONGODB_URI" | sed -e 's/^mongodb:\/\///' -e 's/^[^:]*:[^@]*@//' -e 's/\//\\\//g' -e 's/&/\\&/g' ) +else + MONGODB_URI_FOR_SED="$MONGODB_HOST:$MONGODB_PORT\/$MONGODB_NAME" +fi +sed $SED_ARGS "s/System:.*$/System: $MONGODB_URI_FOR_SED-$EXPEDITION_PORT-$REPLICATION_HOST:$REPLICATION_PORT\/$REPLICATION_CHANNEL Started: $HEAD_DATE/g" ./configuration/jetty/version.txt # Apply app parameters from env.sh if [ "$MONGODB_URI" = "" ]; then @@ -104,7 +108,7 @@ fi REPLICATION_PARAMETERS="-Dreplicate.on.start=$REPLICATE_ON_START -Dreplicate.master.servlet.host=$REPLICATE_MASTER_SERVLET_HOST -Dreplicate.master.servlet.port=$REPLICATE_MASTER_SERVLET_PORT -Dreplicate.master.queue.host=$REPLICATE_MASTER_QUEUE_HOST -Dreplicate.master.queue.port=$REPLICATE_MASTER_QUEUE_PORT -Dreplicate.master.exchange.name=$REPLICATE_MASTER_EXCHANGE_NAME -Dreplicate.master.username=$REPLICATE_MASTER_USERNAME -Dreplicate.master.password=$REPLICATE_MASTER_PASSWORD -Dreplicate.master.bearer_token=$REPLICATE_MASTER_BEARER_TOKEN" if [[ $PARAM == "" ]] || [[ $PARAM == "no-update" ]]; then - nohup $JAVA_BINARY -D$SERVER_NAME $ADDITIONAL_JAVA_ARGS -Dcom.sap.sailing.server.name=$SERVER_NAME $APP_PARAMETERS $REPLICATION_PARAMETERS -Djava.io.tmpdir="$PWD/tmp" -Dfile.encoding=cp1252 -Djetty.home=$scriptdir/configuration/jetty -Djava.util.logging.config.file=$scriptdir/configuration/logging.properties -Djava.library.path=$LIB_DIR -Dosgi.shell.telnet.port=$TELNET_PORT -Xmx$MEMORY -jar plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -configuration ./ -clean 2>/dev/null >/dev/null & + nohup "$JAVA_BINARY" -D$SERVER_NAME $ADDITIONAL_JAVA_ARGS -Dcom.sap.sailing.server.name=$SERVER_NAME $APP_PARAMETERS $REPLICATION_PARAMETERS -Djava.io.tmpdir="$PWD/tmp" -Dfile.encoding=cp1252 -Djetty.home=$scriptdir/configuration/jetty -Djava.util.logging.config.file=$scriptdir/configuration/logging.properties -Djava.library.path=$LIB_DIR -Dosgi.shell.telnet.port=$TELNET_PORT -Xmx$MEMORY -jar plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -configuration ./ -clean 2>/dev/null >/dev/null & JAVA_PID=$! echo "Starting server... (PID: $JAVA_PID)" echo "SERVER_NAME: $SERVER_NAME" @@ -161,10 +165,10 @@ if [[ $PARAM == "" ]] || [[ $PARAM == "no-update" ]]; then elif [[ $PARAM == "debug" ]]; then echo "Starting remote debugging session on port 8000 - make sure this port is open and reachable from the outside" JFR_OPTIONS="-XX:+FlightRecorder -Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.rmi.port=7091 -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost" - $JAVA_BINARY -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=*:8000,suspend=y $JFR_OPTIONS -D$SERVER_NAME $ADDITIONAL_JAVA_ARGS -Dcom.sap.sailing.server.name=$SERVER_NAME $APP_PARAMETERS $REPLICATION_PARAMETERS -Djava.io.tmpdir="$PWD/tmp" -Dfile.encoding=cp1252 -Djetty.home=$scriptdir/configuration/jetty -Djava.util.logging.config.file=$scriptdir/configuration/logging.properties -Djava.library.path=$LIB_DIR -Xmx$MEMORY -jar plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -console -configuration ./ -clean + "$JAVA_BINARY" -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=*:8000,suspend=y $JFR_OPTIONS -D$SERVER_NAME $ADDITIONAL_JAVA_ARGS -Dcom.sap.sailing.server.name=$SERVER_NAME $APP_PARAMETERS $REPLICATION_PARAMETERS -Djava.io.tmpdir="$PWD/tmp" -Dfile.encoding=cp1252 -Djetty.home=$scriptdir/configuration/jetty -Djava.util.logging.config.file=$scriptdir/configuration/logging.properties -Djava.library.path=$LIB_DIR -Xmx$MEMORY -jar plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -console -configuration ./ -clean elif [[ $PARAM == "fg" ]]; then - exec $JAVA_BINARY -D$SERVER_NAME $ADDITIONAL_JAVA_ARGS -Dcom.sap.sailing.server.name=$SERVER_NAME $APP_PARAMETERS $REPLICATION_PARAMETERS -Djava.io.tmpdir="$PWD/tmp" -Dfile.encoding=cp1252 -Djetty.home=$scriptdir/configuration/jetty -Djava.util.logging.config.file=$scriptdir/configuration/logging.properties -Djava.library.path=$LIB_DIR -Xmx$MEMORY -jar plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -console -configuration ./ -clean + exec "$JAVA_BINARY" -D$SERVER_NAME $ADDITIONAL_JAVA_ARGS -Dcom.sap.sailing.server.name=$SERVER_NAME $APP_PARAMETERS $REPLICATION_PARAMETERS -Djava.io.tmpdir="$PWD/tmp" -Dfile.encoding=cp1252 -Djetty.home=$scriptdir/configuration/jetty -Djava.util.logging.config.file=$scriptdir/configuration/logging.properties -Djava.library.path=$LIB_DIR -Xmx$MEMORY -jar plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -console -configuration ./ -clean fi diff --git a/wiki/info/landscape/amazon-ec2.md b/wiki/info/landscape/amazon-ec2.md index 7626bccd956..cc82ac9dc25 100644 --- a/wiki/info/landscape/amazon-ec2.md +++ b/wiki/info/landscape/amazon-ec2.md @@ -29,7 +29,7 @@ INSTALL_FROM_RELEASE=(name-of-release) USE_ENVIRONMENT=live-master-server SERVER_NAME=myspecificevent REPLICATION_CHANNEL=myspecificevent -MONGODB_URI="mongodb://mongo0.internal.sapsailing.com,mongo1.internal.sapsailing.com/myspecificevent?replicaSet=live&retryWrites=true" +MONGODB_URI="mongodb://mongo0.internal.sapsailing.com,mongo1.internal.sapsailing.com/myspecificevent?replicaSet=live&retryWrites=true&readPreference=nearest" # Provide authentication credentials for a user on security-service.sapsailing.com permitted to replicate, either by username/password... #REPLICATE_MASTER_USERNAME=(user for replicator login on security-service.sapsailing.com server having SERVER:REPLICATE:<server-name> permission) #REPLICATE_MASTER_PASSWORD=(password of the user for replication login on security-service.sapsailing.com) @@ -65,7 +65,7 @@ REPLICATE_MASTER_EXCHANGE_NAME=myspecificevent # https://master-server.sapsailing.com/security/api/restsecurity/access_token REPLICATE_MASTER_BEARER_TOKEN=(a bearer token allowing this master to replicate from your master) SERVER_NAME=MYSPECIFICEVENT -MONGODB_URI="mongodb://mongo0.internal.sapsailing.com,mongo1.internal.sapsailing.com/myspecificevent-replica?replicaSet=live&retryWrites=true" +MONGODB_URI="mongodb://mongo0.internal.sapsailing.com,mongo1.internal.sapsailing.com/myspecificevent-replica?replicaSet=live&retryWrites=true&readPreference=nearest" EVENT_ID={some-uuid-of-an-event-you-want-to-feature} SERVER_STARTUP_NOTIFY=you@email.com ``` @@ -641,4 +641,4 @@ Should you want to compare servers of which you know they have different sets of Network InterfacesVirtual network interfaces that are mapped to physical network interfaces on instances. Multi instanceApp instance that runs along with other app instances on the same EC2 instancePlacement GroupsEnables applications to get the full-bisection bandwidth and low-latency network performance required for tightly coupled, node-to-node communication. Placement Groups can only contain HVM instance and have other limitations described here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using_cluster_computing.html - \ No newline at end of file + diff --git a/wiki/info/landscape/creating-ec2-mongodb-image-from-scratch.md b/wiki/info/landscape/creating-ec2-mongodb-image-from-scratch.md index 5ab12190ecb..aae1ba48444 100644 --- a/wiki/info/landscape/creating-ec2-mongodb-image-from-scratch.md +++ b/wiki/info/landscape/creating-ec2-mongodb-image-from-scratch.md @@ -99,7 +99,7 @@ Then execute ``systemctl enable mongod.service`` and ``systemctl enable mongo-re ## Manual MongoDB Replica Set Configuration -In case you decide to skip the benefits of ``mongo-replica-set.service``, you can also establish the connection with the replica set as follows. Connect to the MongoDB on the instance. If it is the first instance of a replica set, issue the command ``rs.initiate()`` in order to turn the instance into the "seed" of a replica set. You can then, for the time being, ``quit()`` the mongo shell. Re-connecting, e.g., with ``mongo "mongodb://localhost:27017/?replicaSet=live"``, will show the ``PRIMARY`` of the new replica set. Don't issue these commands if you only want to add another replica to an existing replica set. +In case you decide to skip the benefits of ``mongo-replica-set.service``, you can also establish the connection with the replica set as follows. Connect to the MongoDB on the instance. If it is the first instance of a replica set, issue the command ``rs.initiate()`` in order to turn the instance into the "seed" of a replica set. You can then, for the time being, ``quit()`` the mongo shell. Re-connecting, e.g., with ``mongo "mongodb://localhost:27017/?replicaSet=live&readPreference=nearest"``, will show the ``PRIMARY`` of the new replica set. Don't issue these commands if you only want to add another replica to an existing replica set. Connected to the PRIMARY using the ``mongo`` shell, a replica can be added using ``rs.add({host: "hostname:port"})``. For additional options see [here](https://docs.mongodb.com/manual/reference/method/rs.add/). To add a hidden replica on ``dbserver.internal.sapsailing.com:10203``, use ``rs.add({host: "dbserver.internal.sapsailing.com:10203", buildIndexes: false, hidden: true, priority: 0})``. @@ -113,4 +113,4 @@ Instead, the ``launch-mongodb-replica.sh`` script can be used to fire up a repli ``configuration/aws-automation/launch-mongodb-replica.sh -r archive -p dbserver.internal.sapsailing.com:10201 -P 0 -t i2.xlarge -k `` -You can then check the replication status by connecting your ``mongo`` client to ``dbserver.internal.sapsailing.com:10201`` and check the output of ``rs.status()``. As long as the new instance is listed in status ``STARTUP2`` it is still synchronizing from the primary. Eventually (took 55min during the last test) it transitions to status ``SECONDARY``. You can then use a connection string such as ``mongodb://dbserver.internal.sapsailing.com:10201/winddb?replicaSet=archive&retryWrites=true&readPreference=secondary`` for the ``MONGODB_URI`` in your archive server Java instance which will then preferably connect to your new fast secondary replica. Consider that the new secondary replica will have to build indices when first queried. This can take another 20 minutes. When done loading the archive server, terminate the instance which will properly remove the replica from the replica set. \ No newline at end of file +You can then check the replication status by connecting your ``mongo`` client to ``dbserver.internal.sapsailing.com:10201`` and check the output of ``rs.status()``. As long as the new instance is listed in status ``STARTUP2`` it is still synchronizing from the primary. Eventually (took 55min during the last test) it transitions to status ``SECONDARY``. You can then use a connection string such as ``mongodb://dbserver.internal.sapsailing.com:10201/winddb?replicaSet=archive&retryWrites=true&readPreference=secondary`` for the ``MONGODB_URI`` in your archive server Java instance which will then preferably connect to your new fast secondary replica. Consider that the new secondary replica will have to build indices when first queried. This can take another 20 minutes. When done loading the archive server, terminate the instance which will properly remove the replica from the replica set.