/*
* Copyright 2005-2016 Red Hat, Inc.
*
* Red Hat licenses this file to you under the Apache License, version
* 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package io.fabric8.maven;
import io.fabric8.kubernetes.api.KubernetesHelper;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerPort;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.ReplicationController;
import io.fabric8.kubernetes.api.model.ReplicationControllerSpec;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceList;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServiceSpec;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.maven.support.DefaultExcludedEnvVariablesEnum;
import io.fabric8.maven.support.DockerCommandPlainPrint;
import io.fabric8.maven.support.IDockerCommandPlainPrintCostants;
import io.fabric8.maven.support.OrderedProperties;
import io.fabric8.openshift.api.model.Route;
import io.fabric8.openshift.api.model.RouteList;
import io.fabric8.openshift.api.model.RouteListBuilder;
import io.fabric8.openshift.api.model.RouteSpec;
import io.fabric8.openshift.api.model.RouteTargetReference;
import io.fabric8.openshift.client.OpenShiftClient;
import io.fabric8.utils.Files;
import io.fabric8.utils.Strings;
import io.fabric8.utils.TablePrinter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
/**
* Generates a properties file that contains that env variables that are expected to be passed by kubernetes to the container.
* The env variable configuration is determined by the kubernetes.json file and the services that are currently running in the target namespace.
*/
@Mojo(name = "create-env", defaultPhase = LifecyclePhase.COMPILE)
public class CreateEnvMojo extends AbstractFabric8Mojo {
private static final String HOST_SUFFIX = "_SERVICE_HOST";
private static final String PORT_SUFFIX = "_SERVICE_PORT";
private static final String PROTO_SUFFIX = "_TCP_PROTO";
private static final String DOCKE_ENV_PREFIX = "docker.env.";
private static final String DOCKER_NAME = "docker.name";
private static final String EXEC_ENV_SCRIPT = "environmentScript";
@Parameter(property = "fabric8.envFile", defaultValue = "env.properties")
private String envPropertiesFileName;
@Parameter(property = "fabric8.envScript", defaultValue = "env.sh")
private String envScriptFileName;
@Parameter(property = "fabric8.dockerRunScript", defaultValue = "docker-run.sh")
private String dockerRunScriptFileName;
// the docker image name we will use
private volatile String name;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
try {
String basedir = System.getProperty("basedir", ".");
File propertiesFile = new File(basedir + "/target/" + envPropertiesFileName).getCanonicalFile();
File scriptFile = new File(basedir + "/target/" + envScriptFileName).getCanonicalFile();
File dockerRunFile = new File(basedir + "/target/" + dockerRunScriptFileName).getCanonicalFile();
Object config = loadKubernetesJson();
List<HasMetadata> list = KubernetesHelper.toItemList(config);
name = getDockerImage();
if (name == null) {
name = findFirstImageName(list);
}
Map<String, String> env = getEnvFromConfig(list);
String namespace = getNamespace();
env.putAll(getNamespaceServiceEnv(namespace));
removeDefaultEnv(env);
expandEnvironmentVariable(env);
displayEnv(env);
StringBuilder sb = new StringBuilder();
List<VolumeMount> volumeMount = getVolumeMountsFromConfig(list);
List<ContainerPort> containerPort = getContainerPortsFromConfig(list);
DockerCommandPlainPrint dockerCommandPlainPrint = new DockerCommandPlainPrint(sb);
dockerCommandPlainPrint.appendParameters(env, IDockerCommandPlainPrintCostants.EXPRESSION_FLAG);
dockerCommandPlainPrint.appendContainerPorts(containerPort, IDockerCommandPlainPrintCostants.PORT_FLAG);
dockerCommandPlainPrint.appendVolumeMounts(volumeMount, IDockerCommandPlainPrintCostants.VOLUME_FLAG);
dockerCommandPlainPrint.appendImageName(name);
displayVolumes(volumeMount);
displayContainerPorts(containerPort);
displayDockerRunCommand(dockerCommandPlainPrint);
Properties properties = getProjectAndFabric8Properties(getProject());
for (Map.Entry<String, String> entry : env.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key == null) {
getLog().warn("Ignoring null key!");
} else if (value == null) {
getLog().warn("Ignoring null value for key: " + key);
} else {
properties.setProperty(DOCKE_ENV_PREFIX + key, value);
}
}
if (name != null) {
properties.setProperty(DOCKER_NAME, name);
}
String scriptFileAbsolutePath = scriptFile.getAbsolutePath();
if (scriptFileAbsolutePath != null) {
properties.setProperty(EXEC_ENV_SCRIPT, scriptFileAbsolutePath);
}
Properties envProperties = new OrderedProperties();
Set<Map.Entry<String, String>> entries = env.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
if (key == null) {
getLog().warn("Ignoring null key!");
} else if (value == null) {
getLog().warn("Ignoring null value for key: " + key);
} else {
envProperties.put(key ,value);
}
}
saveEnvScript(env, scriptFile);
saveDockerRunScript(dockerCommandPlainPrint, dockerRunFile);
saveProperties(envProperties, propertiesFile);
} catch (IOException e) {
throw new MojoExecutionException("Failed to load environment schemas: " + e, e);
}
}
/**
* Returns the first docker image name found in a ReplicationController
*/
protected String findFirstImageName(List<HasMetadata> list) {
for (HasMetadata hasMetadata : list) {
if (hasMetadata instanceof ReplicationController) {
ReplicationController rc = (ReplicationController) hasMetadata;
ReplicationControllerSpec spec = rc.getSpec();
if (spec != null) {
PodTemplateSpec podTemplateSpec = spec.getTemplate();
if (podTemplateSpec != null) {
PodSpec podSpec = podTemplateSpec.getSpec();
if (podSpec != null) {
List<Container> containers = podSpec.getContainers();
if (containers != null) {
for (Container container : containers) {
String image = container.getImage();
if (Strings.isNotBlank(image)) {
return image;
}
}
}
}
}
}
}
}
return null;
}
/**
* Return the Env Variables that correspond to each services that runs under the specified namespace.
*
* @param namespace The target namespace.
* @return the env variables
*/
Map<String, String> getNamespaceServiceEnv(String namespace) {
Map<String, String> result = new HashMap<>();
KubernetesClient kubernetes = getKubernetes();
ServiceList serviceList = kubernetes.services().inNamespace(namespace).list();
RouteList routeList = listRoutes(kubernetes, namespace);
for (Service service : serviceList.getItems()) {
String serviceName = KubernetesHelper.getName(service);
String id = serviceName.toUpperCase().replace("-", "_");
Route route = findRoute(serviceName, routeList);
RouteSpec spec = null;
if (route != null) {
spec = route.getSpec();
}
ServiceSpec serviceSpec = service.getSpec();
if (spec != null) {
result.put(id + HOST_SUFFIX, spec.getHost());
} else if (serviceSpec != null) {
result.put(id + HOST_SUFFIX, serviceSpec.getClusterIP());
}
if (serviceSpec != null) {
List<ServicePort> ports = serviceSpec.getPorts();
for (ServicePort port : ports) {
result.put(id + PORT_SUFFIX, String.valueOf(port.getPort()));
result.put(id + PROTO_SUFFIX, port.getProtocol());
result.put(id + PORT_SUFFIX + "_" + port.getPort() + PROTO_SUFFIX, port.getProtocol());
}
}
}
return result;
}
/**
* Return Env variables found in the kubernetes config.
*
* @param entities The config instance.
* @return A map with the env key value pairs.
* @throws IOException
*/
private Map<String, String> getEnvFromConfig(List<HasMetadata> entities) throws IOException {
Map<String, String> result = new TreeMap<>();
for (HasMetadata entity : entities) {
if (entity instanceof Pod) {
Pod pod = (Pod) entity;
for (Container container : pod.getSpec().getContainers()) {
if (container.getImage().equals(name)) {
result.putAll(mapFromEnv(container.getEnv()));
}
}
} else if (entity instanceof ReplicationController) {
ReplicationController replicationController = (ReplicationController) entity;
for (Container container : replicationController.getSpec().getTemplate().getSpec().getContainers()) {
if (container.getImage().equals(name)) {
result.putAll(mapFromEnv(container.getEnv()));
}
}
}
}
return result;
}
/**
* Lets expand environment variables by overriding it via via the command
* line.
*/
protected void expandEnvironmentVariable(Map<String, String> env) {
String regex = "\\$\\{(.*?)\\}";
MavenProject project = getProject();
if (project != null) {
Properties properties = getProjectAndFabric8Properties(project);
for (Map.Entry<String, String> entry : env.entrySet()) {
String envValue = entry.getValue();
if (envValue != null && !envValue.isEmpty() && envValue.contains("${") && envValue.contains("}")) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(entry.getValue());
while (m.find()) {
String parameterName = m.group(1);
String name = "fabric8.create-env." + parameterName;
String propertyValue = properties.getProperty(name);
if (Strings.isNotBlank(propertyValue)) {
getLog().info("Overriding environment variable " + parameterName + " with value: " + propertyValue);
String replaced = checkAndReplaceValue(entry.getValue(), propertyValue, parameterName);
entry.setValue(replaced);
} else {
getLog().info("No property defined for environment variable: " + parameterName);
}
}
}
}
}
}
/**
* Return VolumeMounts found in the kubernetes config.
*
* @param entities The config instance.
* @return A list of VolumeMount objects.
* @throws IOException
*/
private List<VolumeMount> getVolumeMountsFromConfig(List<HasMetadata> entities) throws IOException {
List<VolumeMount> volumeList = new ArrayList<VolumeMount>();
for (HasMetadata entity : entities) {
if (entity instanceof ReplicationController) {
ReplicationController replicationController = (ReplicationController) entity;
for (Container container : replicationController.getSpec().getTemplate().getSpec().getContainers()) {
if (container.getImage().equals(name)) {
if (!container.getVolumeMounts().isEmpty()) {
return container.getVolumeMounts();
}
}
}
}
}
return volumeList;
}
/**
* Return container Ports found in the kubernetes config.
*
* @param entities The config instance.
* @return A list of ContainerPort objects.
* @throws IOException
*/
private List<ContainerPort> getContainerPortsFromConfig(List<HasMetadata> entities) throws IOException {
List<ContainerPort> containerPortList = new ArrayList<ContainerPort>();
for (HasMetadata entity : entities) {
if (entity instanceof Pod) {
Pod pod = (Pod) entity;
for (Container container : pod.getSpec().getContainers()) {
if (container.getImage().equals(name)) {
if (!container.getPorts().isEmpty()) {
containerPortList.addAll(container.getPorts());
}
}
}
}
else if (entity instanceof ReplicationController) {
ReplicationController replicationController = (ReplicationController) entity;
for (Container container : replicationController.getSpec().getTemplate().getSpec().getContainers()) {
if (container.getImage().equals(name)) {
if (!container.getPorts().isEmpty()) {
containerPortList.addAll(container.getPorts());
}
}
}
}
}
return containerPortList;
}
private void displayEnv(Map<String, String> map) {
TablePrinter table = new TablePrinter();
table.columns("Name", "Value");
for (Map.Entry<String, String> entry : map.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
table.row(name, value);
}
getLog().info("");
getLog().info("Generated Environment variables:");
getLog().info("-------------------------------");
List<String> lines = table.asTextLines();
for (String line : lines) {
getLog().info(line);
}
getLog().info("");
}
private void displayVolumes(List<VolumeMount> volumeMount) {
TablePrinter table = new TablePrinter();
table.columns("Name", "Mount Path", "Read Only");
Iterator<VolumeMount> it = volumeMount.iterator();
while (it.hasNext()){
VolumeMount vol = it.next();
String name = vol.getName();
String mounthPath = vol.getMountPath();
String ro = String.valueOf(Boolean.FALSE);
if (vol.getReadOnly() == true) {
ro = String.valueOf(Boolean.TRUE);
}
table.row(name, mounthPath, ro);
}
getLog().info("");
getLog().info("Volumes Summary:");
getLog().info("-------------------------------");
List<String> lines = table.asTextLines();
for (String line : lines) {
getLog().info(line);
}
getLog().info("");
}
private void displayContainerPorts(List<ContainerPort> containerPort) {
TablePrinter table = new TablePrinter();
table.columns("Host IP", "Host Port", "Container Port");
Iterator<ContainerPort> it = containerPort.iterator();
while (it.hasNext()){
ContainerPort port = it.next();
String hostIp = port.getHostIP();
String hostPort = "";
String contPort = "";
if (port.getHostPort() != null) {
hostPort = String.valueOf(port.getHostPort());
}
if (port.getContainerPort() != null) {
contPort = String.valueOf(port.getContainerPort());
}
table.row(hostIp, hostPort, contPort);
}
getLog().info("");
getLog().info("Container Ports Summary:");
getLog().info("-------------------------------");
List<String> lines = table.asTextLines();
for (String line : lines) {
getLog().info(line);
}
getLog().info("");
}
private void removeDefaultEnv(Map<String, String> map) {
for(Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, String> entry = it.next();
if(DefaultExcludedEnvVariablesEnum.contains(entry.getKey())) {
it.remove();
}
}
}
private void displayDockerRunCommand(DockerCommandPlainPrint dockerCommandPlainPrint) {
getLog().info("Docker Run Command:");
getLog().info("-------------------------------");
getLog().info(dockerCommandPlainPrint.getDockerPlainTextCommand().toString());
}
/**
* Load the kubernetes configuration found in the project
*
* @return The Kubernetes config.
*/
private Object loadKubernetesJson() throws MojoFailureException, IOException {
File json = getKubernetesJson();
if (!Files.isFile(json)) {
if (Files.isFile(kubernetesSourceJson)) {
json = kubernetesSourceJson;
} else {
throw new MojoFailureException("No such generated kubernetes json file: " + json + " or source json file " + kubernetesSourceJson);
}
}
return KubernetesHelper.loadJson(json);
}
private static Map<String, String> mapFromEnv(List<EnvVar> envVars) {
Map<String, String> result = new HashMap<>();
for (EnvVar envVar : envVars) {
result.put(envVar.getName(), envVar.getValue());
}
return result;
}
private static void saveProperties(Properties properties, File propertiesFile) throws IOException {
try (FileWriter writer = new FileWriter(propertiesFile)) {
properties.store(writer, "Generated Environment Variables");
}
}
private static void saveEnvScript(Map<String, String> map, File scroptFile) throws IOException {
try (FileWriter writer = new FileWriter(scroptFile)) {
writer.append("#!/bin/bash").append("\n");
for (Map.Entry<String, String> entry: map.entrySet()) {
if (entry.getValue() != null && !entry.getValue().contains(" ")) {
writer.append("export ").append(entry.getKey()).append("=").append(entry.getValue()).append("\n");
} else {
writer.append("export ").append(entry.getKey()).append("=").append("\"").append(entry.getValue()).append("\"").append("\n");
}
}
writer.flush();
}
}
private static void saveDockerRunScript(DockerCommandPlainPrint printer, File scriptFile) throws IOException {
try (FileWriter writer = new FileWriter(scriptFile)) {
writer.append("#!/bin/bash").append("\n");
writer.append(printer.getDockerPlainTextCommand().toString()).append("\n");
writer.flush();
}
}
private static RouteList listRoutes(KubernetesClient client, String namespace) {
try {
return client.adapt(OpenShiftClient.class).routes().inNamespace(namespace).list();
} catch (Throwable t) {
return new RouteListBuilder().build();
}
}
private static Route findRoute(String serviceId, RouteList routeList) {
for (Route route : routeList.getItems()) {
RouteSpec spec = route.getSpec();
if (spec != null) {
RouteTargetReference to = spec.getTo();
if (to != null) {
String name = to.getName();
if (serviceId.equals(name)) {
return route;
}
}
}
}
return null;
}
private static String checkAndReplaceValue(String envValue, String value, String parameterName) {
String escapedParameterName = "\\$\\{" + parameterName + "\\}";
String stringReplaced = envValue.replaceAll(escapedParameterName, value);
return stringReplaced;
}
}