diff --git a/cli/src/main/java/org/wildfly/glow/cli/commands/Constants.java b/cli/src/main/java/org/wildfly/glow/cli/commands/Constants.java index 9a636267..02d2a180 100644 --- a/cli/src/main/java/org/wildfly/glow/cli/commands/Constants.java +++ b/cli/src/main/java/org/wildfly/glow/cli/commands/Constants.java @@ -56,6 +56,9 @@ public interface Constants { String HELP_OPTION = "--help"; String HELP_OPTION_SHORT = "-h"; + String INIT_SCRIPT_OPTION = "--init-script"; + String INIT_SCRIPT_OPTION_SHORT = "-id"; + String INIT_SCRIPT_OPTION_LABEL = ""; String INPUT_FEATURE_PACKS_FILE_OPTION = "--input-feature-packs-file"; String INPUT_FEATURE_PACKS_FILE_OPTION_LABEL = ""; String NO_DOCKER_IMAGE_OPTION = "--no-docker-image"; diff --git a/cli/src/main/java/org/wildfly/glow/cli/commands/OpenShiftSupport.java b/cli/src/main/java/org/wildfly/glow/cli/commands/OpenShiftSupport.java index 39e824b5..af692ee1 100644 --- a/cli/src/main/java/org/wildfly/glow/cli/commands/OpenShiftSupport.java +++ b/cli/src/main/java/org/wildfly/glow/cli/commands/OpenShiftSupport.java @@ -23,6 +23,7 @@ import io.fabric8.kubernetes.api.model.HTTPGetAction; import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.ObjectReference; +import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; @@ -39,16 +40,22 @@ import io.fabric8.openshift.api.model.BuildConfig; import io.fabric8.openshift.api.model.BuildConfigBuilder; import io.fabric8.openshift.api.model.ImageLookupPolicy; +import io.fabric8.openshift.api.model.ImageSource; +import io.fabric8.openshift.api.model.ImageSourceBuilder; +import io.fabric8.openshift.api.model.ImageSourcePath; +import io.fabric8.openshift.api.model.ImageSourcePathBuilder; import io.fabric8.openshift.api.model.ImageStream; import io.fabric8.openshift.api.model.ImageStreamBuilder; import io.fabric8.openshift.api.model.Route; import io.fabric8.openshift.api.model.RouteBuilder; import io.fabric8.openshift.api.model.RouteTargetReference; import io.fabric8.openshift.api.model.TLSConfig; +import io.fabric8.openshift.api.model.TagReferenceBuilder; import io.fabric8.openshift.client.OpenShiftClient; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -59,6 +66,13 @@ import java.util.TreeMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jboss.galleon.api.GalleonBuilder; +import org.jboss.galleon.api.Provisioning; +import org.jboss.galleon.api.config.GalleonConfigurationWithLayers; +import org.jboss.galleon.api.config.GalleonFeaturePackConfig; +import org.jboss.galleon.api.config.GalleonProvisioningConfig; +import org.jboss.galleon.config.ConfigId; +import org.jboss.galleon.util.IoUtils; import org.jboss.galleon.util.ZipUtils; import org.wildfly.glow.AddOn; import org.wildfly.glow.GlowMessageWriter; @@ -92,6 +106,25 @@ private static void createAppDeployment(GlowMessageWriter writer, Path target, O for (Entry entry : env.entrySet()) { vars.add(new EnvVar().toBuilder().withName(entry.getKey()).withValue(entry.getValue()).build()); } + if (ha) { + writer.info("\n HA enabled, 2 replicas will be started."); + vars.add(new EnvVar().toBuilder().withName("JGROUPS_PING_PROTOCOL").withValue("DNS_PING").build()); + vars.add(new EnvVar().toBuilder().withName("OPENSHIFT_DNS_PING_SERVICE_PORT").withValue("8888").build()); + vars.add(new EnvVar().toBuilder().withName("OPENSHIFT_DNS_PING_SERVICE_NAME").withValue(name + "-ping").build()); + IntOrString v = new IntOrString(); + v.setValue(8888); + Service pingService = new ServiceBuilder().withNewMetadata().withName(name + "-ping").endMetadata(). + withNewSpec().withPorts(new ServicePort().toBuilder().withProtocol("TCP"). + withPort(8888). + withName("ping"). + withTargetPort(v).build()). + withClusterIP("None").withPublishNotReadyAddresses().withIpFamilies("IPv4"). + withInternalTrafficPolicy("Cluster").withClusterIPs("None"). + withType("ClusterIP").withIpFamilyPolicy("SingleStack"). + withSessionAffinity("None").withSelector(labels).endSpec().build(); + osClient.services().resource(pingService).createOr(NonDeletingOperation::update); + Files.write(target.resolve(name + "-ping-service.yaml"), Serialization.asYaml(pingService).getBytes()); + } Container container = new Container(); container.setName(name); container.setImage(name + ":latest"); @@ -148,7 +181,7 @@ private static void createAppDeployment(GlowMessageWriter writer, Path target, O } static void deploy(GlowMessageWriter writer, Path target, String appName, Map env, Set layers, Set addOns, boolean ha, - Map extraEnv, Set disabledDeployers) throws Exception { + Map extraEnv, Set disabledDeployers, Path initScript) throws Exception { Map actualEnv = new TreeMap<>(); OpenShiftClient osClient = new KubernetesClientBuilder().build().adapt(OpenShiftClient.class); writer.info("\nConnected to OpenShift cluster"); @@ -205,8 +238,8 @@ static void deploy(GlowMessageWriter writer, Path target, String appName, Map disabledDeployers) { return disabledDeployers.contains("ALL") || disabledDeployers.contains(name); } - static void createBuild(GlowMessageWriter writer, Path target, OpenShiftClient osClient, String name) throws Exception { - // zip deployment and provisioning.xml to be pushed to OpenShift - Path file = Paths.get("openshiftApp.zip"); - if (Files.exists(file)) { - Files.delete(file); + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (int i = 0; i < hash.length; i++) { + String hex = Integer.toHexString(0xff & hash[i]); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + static Map createLabels(Path provisioning) throws Exception { + GalleonBuilder provider = new GalleonBuilder(); + Path dir = provisioning.getParent().resolve("tmpHome"); + Files.createDirectory(dir); + StringBuilder layers = new StringBuilder(); + StringBuilder excludedLayers = new StringBuilder(); + StringBuilder fps = new StringBuilder(); + Map labels = new HashMap<>(); + try (Provisioning p = provider.newProvisioningBuilder(provisioning).setInstallationHome(dir).build()) { + GalleonProvisioningConfig config = provider.newProvisioningBuilder(provisioning).setInstallationHome(dir).build().loadProvisioningConfig(provisioning); + GalleonConfigurationWithLayers cl = config.getDefinedConfig(new ConfigId("standalone", "standalone.xml")); + for(String s : cl.getIncludedLayers()) { + labels.put("org.wildfly.glow.layer."+s,""); + } + for(String s : cl.getExcludedLayers()) { + labels.put("org.wildfly.glow.excluded.layer."+s,""); + } + for (GalleonFeaturePackConfig gfpc : config.getFeaturePackDeps()) { + if (fps.length() != 0) { + fps.append("_"); + } + String producerName = gfpc.getLocation().getProducerName(); + producerName = producerName.replaceAll("::zip", ""); + int i = producerName.indexOf(":"); + if(i > 0) { + producerName = producerName.substring(i+1); + } + producerName = producerName.replaceAll(":", "-"); + labels.put("org.wildfly.glow.feature-pack."+producerName,""); + } + } + return labels; + } + + private static String format(String label) { + if (label.length() > 63) { + label = label.substring(0, 56); + label += ".trunc"; + } + return label; + } + static String doServerImageBuild(GlowMessageWriter writer, Path target, OpenShiftClient osClient) throws Exception { + Path provisioning = target.resolve("galleon").resolve("provisioning.xml"); + byte[] content = Files.readAllBytes(provisioning); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] encodedhash = digest.digest(content); + String key = bytesToHex(encodedhash); + String serverImageName = "wildfly-server-" + key; + + ImageStream stream = new ImageStreamBuilder().withNewMetadata().withName(serverImageName). + endMetadata().withNewSpec().withLookupPolicy(new ImageLookupPolicy(Boolean.TRUE)).endSpec().build(); + // check if it exists + ImageStream existingStream = osClient.imageStreams().resource(stream).get(); + if (existingStream == null) { + writer.info("\nBuilding server image (this can take up to few minutes the first time)..."); + // zip deployment and provisioning.xml to be pushed to OpenShift + Path file = Paths.get("openshiftServer.zip"); + if (Files.exists(file)) { + Files.delete(file); + } + file.toFile().deleteOnExit(); + // First do a build of the naked server + Path stepOne = target.resolve("step-one"); + Files.createDirectories(stepOne); + IoUtils.copy(target.resolve("galleon"), stepOne.resolve("galleon")); + ZipUtils.zip(stepOne, file); + stream = stream.toBuilder().editOrNewMetadata().withLabels(createLabels(provisioning)).endMetadata().build(); + osClient.imageStreams().resource(stream).createOr(NonDeletingOperation::update); + Files.write(target.resolve(serverImageName + "-image-stream.yaml"), Serialization.asYaml(stream).getBytes()); + BuildConfigBuilder builder = new BuildConfigBuilder(); + ObjectReference ref = new ObjectReference(); + ref.setKind("ImageStreamTag"); + ref.setName(serverImageName + ":latest"); + BuildConfig buildConfig = builder. + withNewMetadata().withName(serverImageName + "-build").endMetadata().withNewSpec(). + withNewOutput(). + withNewTo(). + withKind("ImageStreamTag"). + withName(serverImageName + ":latest").endTo(). + endOutput().withNewStrategy().withNewSourceStrategy().withNewFrom().withKind("DockerImage"). + withName("quay.io/wildfly/wildfly-s2i:latest").endFrom(). + withIncremental(true). + withEnv(new EnvVar().toBuilder().withName("GALLEON_USE_LOCAL_FILE").withValue("true").build()). + endSourceStrategy().endStrategy().withNewSource(). + withType("Binary").endSource().endSpec().build(); + osClient.buildConfigs().resource(buildConfig).createOr(NonDeletingOperation::update); + Files.write(target.resolve(serverImageName + "-build-config.yaml"), Serialization.asYaml(buildConfig).getBytes()); + + Build build = osClient.buildConfigs().withName(serverImageName + "-build").instantiateBinary().fromFile(file.toFile()); + CountDownLatch latch = new CountDownLatch(1); + try (Watch watcher = osClient.builds().withName(build.getMetadata().getName()).watch(getBuildWatcher(writer, latch))) { + latch.await(); + } } - file.toFile().deleteOnExit(); - ZipUtils.zip(target, file); - writer.info("\nCreating and starting application image build on OpenShift (this can take up to few minutes)..."); - ImageStream stream = new ImageStreamBuilder().withNewMetadata().withName(name). + return serverImageName; + } + + static void doAppImageBuild(String serverImageName, GlowMessageWriter writer, Path target, OpenShiftClient osClient, String name, Path initScript) throws Exception { + // Now step 2 + // From the server image, do a docker build, copy the server and copy in it the deployments and init file. + Path stepTwo = target.resolve("step-two"); + IoUtils.copy(target.resolve("deployments"), stepTwo.resolve("deployments")); + StringBuilder dockerFileBuilder = new StringBuilder(); + dockerFileBuilder.append("FROM wildfly-runtime:latest\n"); + dockerFileBuilder.append("COPY --chown=jboss:root /server $JBOSS_HOME\n"); + dockerFileBuilder.append("COPY --chown=jboss:root deployments/* $JBOSS_HOME/standalone/deployments\n"); + + if (initScript != null) { + packageInitScript(initScript, stepTwo); + dockerFileBuilder.append("COPY --chown=jboss:root extensions $JBOSS_HOME/extensions\n"); + dockerFileBuilder.append("RUN chmod ug+rwx $JBOSS_HOME/extensions/postconfigure.sh\n"); + } + + dockerFileBuilder.append("RUN chmod -R ug+rwX $JBOSS_HOME\n"); + + Path dockerFile = stepTwo.resolve("Dockerfile"); + Files.write(dockerFile, dockerFileBuilder.toString().getBytes()); + Path file2 = Paths.get("openshiftApp.zip"); + if (Files.exists(file2)) { + Files.delete(file2); + } + ZipUtils.zip(stepTwo, file2); + writer.info("\nCreating and starting application image build on OpenShift..."); + ImageStream runtimeStream = new ImageStreamBuilder().withNewMetadata().withName("wildfly-runtime"). + endMetadata().withNewSpec(). + addToTags(0, new TagReferenceBuilder() + .withName("latest") + .withFrom(new ObjectReferenceBuilder() + .withKind("DockerImage") + .withName("quay.io/wildfly/wildfly-runtime:latest") + .build()) + .build()). + withLookupPolicy(new ImageLookupPolicy(Boolean.TRUE)).endSpec().build(); + osClient.imageStreams().resource(runtimeStream).createOr(NonDeletingOperation::update); + ImageStream appStream = new ImageStreamBuilder().withNewMetadata().withName(name). endMetadata().withNewSpec().withLookupPolicy(new ImageLookupPolicy(Boolean.TRUE)).endSpec().build(); - osClient.imageStreams().resource(stream).createOr(NonDeletingOperation::update); - Files.write(target.resolve(name + "-image-stream.yaml"), Serialization.asYaml(stream).getBytes()); + osClient.imageStreams().resource(appStream).createOr(NonDeletingOperation::update); BuildConfigBuilder builder = new BuildConfigBuilder(); ObjectReference ref = new ObjectReference(); ref.setKind("ImageStreamTag"); - ref.setName(name + ":latest"); - BuildConfig buildConfig = builder. + ref.setName(serverImageName + ":latest"); + ImageSourcePath srcPath = new ImageSourcePathBuilder().withSourcePath("/opt/server").withDestinationDir(".").build(); + ImageSource imageSource = new ImageSourceBuilder().withFrom(ref).withPaths(srcPath).build(); + BuildConfig buildConfig2 = builder. withNewMetadata().withName(name + "-build").endMetadata().withNewSpec(). withNewOutput(). withNewTo(). withKind("ImageStreamTag"). withName(name + ":latest").endTo(). - endOutput().withNewStrategy().withNewSourceStrategy().withNewFrom().withKind("DockerImage"). - withName("quay.io/wildfly/wildfly-s2i:latest").endFrom(). - withIncremental(true). - withEnv(new EnvVar().toBuilder().withName("GALLEON_USE_LOCAL_FILE").withValue("true").build()). - endSourceStrategy().endStrategy().withNewSource(). - withType("Binary").endSource().endSpec().build(); - osClient.buildConfigs().resource(buildConfig).createOr(NonDeletingOperation::update); - Files.write(target.resolve(name + "-build-config.yaml"), Serialization.asYaml(buildConfig).getBytes()); + endOutput(). + withNewSource().withType("Binary").withImages(imageSource).endSource(). + withNewStrategy().withNewDockerStrategy().withNewFrom().withKind("ImageStream"). + withName("wildfly-runtime").endFrom(). + withDockerfilePath("./Dockerfile"). + endDockerStrategy().endStrategy().endSpec().build(); + osClient.buildConfigs().resource(buildConfig2).createOr(NonDeletingOperation::update); + Files.write(target.resolve(name + "-build-config.yaml"), Serialization.asYaml(buildConfig2).getBytes()); - Build build = osClient.buildConfigs().withName(name + "-build").instantiateBinary().fromFile(file.toFile()); + Build build = osClient.buildConfigs().withName(name + "-build").instantiateBinary().fromFile(file2.toFile()); CountDownLatch latch = new CountDownLatch(1); try (Watch watcher = osClient.builds().withName(build.getMetadata().getName()).watch(getBuildWatcher(writer, latch))) { latch.await(); diff --git a/cli/src/main/java/org/wildfly/glow/cli/commands/ScanCommand.java b/cli/src/main/java/org/wildfly/glow/cli/commands/ScanCommand.java index 4024dc83..f6dd5958 100644 --- a/cli/src/main/java/org/wildfly/glow/cli/commands/ScanCommand.java +++ b/cli/src/main/java/org/wildfly/glow/cli/commands/ScanCommand.java @@ -114,6 +114,9 @@ public Stability convert(String value) throws Exception { @CommandLine.Option(names = {Constants.ENV_FILE_OPTION_SHORT, Constants.ENV_FILE_OPTION}, paramLabel = Constants.ENV_FILE_OPTION_LABEL) Optional envFile; + @CommandLine.Option(names = {Constants.INIT_SCRIPT_OPTION_SHORT, Constants.INIT_SCRIPT_OPTION}, paramLabel = Constants.INIT_SCRIPT_OPTION_LABEL) + Optional initScriptFile; + @CommandLine.Option(names = Constants.DISABLE_DEPLOYERS, split = ",", paramLabel = Constants.ADD_ONS_OPTION_LABEL) Set disableDeployers = new LinkedHashSet<>(); @@ -168,6 +171,19 @@ public Integer call() throws Exception { } } } + if (initScriptFile.isPresent()) { + if (provision.isPresent()) { + if (!OPENSHIFT.equals(provision.get())) { + throw new Exception("Init script file is only usable when --provision=" + OPENSHIFT + " option is set."); + } + } else { + throw new Exception("Init script file file is only usable when --provision=" + OPENSHIFT + " option is set."); + } + Path p = initScriptFile.get(); + if (!Files.exists(p)) { + throw new Exception(p + " file doesn't exist"); + } + } builder.setVerbose(verbose); if (!addOns.isEmpty()) { builder.setUserEnabledAddOns(addOns); @@ -318,8 +334,10 @@ public Integer call() throws Exception { } if (OutputFormat.OPENSHIFT.equals(provision.get())) { String name = null; + Path deploymentsDir = target.resolve("deployments"); + Files.createDirectories(deploymentsDir); for (Path p : deployments) { - Files.copy(p, target.resolve(p.getFileName())); + Files.copy(p, deploymentsDir.resolve(p.getFileName())); int ext = p.getFileName().toString().indexOf("."); name = p.getFileName().toString().substring(0, ext); } @@ -329,8 +347,8 @@ public Integer call() throws Exception { envMap.put(env.getName(), env.getDescription()); } } - OpenShiftSupport.deploy(GlowMessageWriter.DEFAULT, target, name == null ? "app-from-wildfly-glow" : name, envMap, scanResults.getDiscoveredLayers(), - scanResults.getEnabledAddOns(), haProfile.orElse(false), extraEnv, disableDeployers); + OpenShiftSupport.deploy(GlowMessageWriter.DEFAULT, target, name == null ? "app-from-wildfly-glow" : name.toLowerCase(), envMap, scanResults.getDiscoveredLayers(), + scanResults.getEnabledAddOns(), haProfile.orElse(false), extraEnv, disableDeployers, initScriptFile.orElse(null)); print("@|bold Openshift build and deploy DONE.|@"); } if (content.getDockerImageName() != null) { diff --git a/cli/src/main/resources/UsageMessages.properties b/cli/src/main/resources/UsageMessages.properties index 812db6f9..5630dae7 100644 --- a/cli/src/main/resources/UsageMessages.properties +++ b/cli/src/main/resources/UsageMessages.properties @@ -36,6 +36,8 @@ output-dir = If specifying to provision, the directory where the result will be wildfly-preview = Use only WildFly preview feature-packs as input. stability-level = Specify a stability to be used when provisioning a server. The stability is also used to identify server features that would be not enabled by the specified stability. The stability is by default the minimum stability of each Galleon feature-packs. The stability can be @|fg(yellow) default|@, @|fg(yellow) community|@, @|fg(yellow) preview|@, @|fg(yellow) experimental|@. env-file = The path to a file that contains environment variables (in the form env=value) to be passed to the OpenShift deployment. Can only be used with @|fg(yellow) OPENSHIFT|@ kind of provisioning. +init-script = The path to a script that contains commands (JBoss CLI, add-user, ...) to fine tune the server on OpenShift deployment. Can only be used with @|fg(yellow) OPENSHIFT|@ kind of provisioning. + disable-deployers = A comma separated list of deployer names to disable. To retrieve all the deployer names call the @|fg(yellow) show-configuration|@ operation. To disable them all, use @|fg(yellow) ALL|@ value. Can only be used with @|fg(yellow) OPENSHIFT|@ kind of provisioning. usage.synopsisHeading = %nUsage:\u0020 diff --git a/docs/guide/intro/index.adoc b/docs/guide/intro/index.adoc index 197cbc58..2e867774 100644 --- a/docs/guide/intro/index.adoc +++ b/docs/guide/intro/index.adoc @@ -67,11 +67,16 @@ provisioning and create your application deployment. At the end of the build, the application is deployed and the route to your application inside the cluster is printed. Use it to interact with your application. +Note: the support for OpenShift is currently specified by this WildFly Glow project link:https://github.com/wildfly/wildfly-glow/issues/49[GitHub Issue]. + ###### Automatic deployment of PostGreSQL, MySQL, MariaDB, Artemis JMS Broker and Keycloak If WildFly Glow detects the need for these technologies, it will automatically deploy the required servers and will bound the application to them. -This feature is currently specified by this link:https://github.com/wildfly/wildfly-glow/issues/49[GitHub Issue]. +###### High Availability support + +When the HA profile is enabled, 2 pods will be created for the deployment. +The JGroups `dns.DNS_PING` protocol is enabled for the members of the cluster to discover each others. #### WildFly additional features discovery diff --git a/openshift-deployment/api/pom.xml b/openshift-deployment/api/pom.xml index 40098953..0a184124 100644 --- a/openshift-deployment/api/pom.xml +++ b/openshift-deployment/api/pom.xml @@ -15,6 +15,10 @@ ${project.groupId} wildfly-glow-core + + org.jboss.galleon + galleon-api + io.fabric8 openshift-client