/** * Copyright 2005-2016 Red Hat, Inc. * <p> * 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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 com.fasterxml.jackson.databind.node.TextNode; import io.fabric8.kubernetes.api.Annotations; import io.fabric8.kubernetes.api.KubernetesHelper; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; 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.Secret; import io.fabric8.kubernetes.api.model.extensions.Deployment; import io.fabric8.kubernetes.api.model.extensions.DeploymentSpec; import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; import io.fabric8.kubernetes.internal.HasMetadataComparator; import io.fabric8.maven.support.JsonSchema; import io.fabric8.maven.support.JsonSchemaProperty; import io.fabric8.openshift.api.model.Template; import io.fabric8.utils.DomHelper; import io.fabric8.utils.Files; import io.fabric8.utils.IOHelpers; import io.fabric8.utils.Strings; import io.fabric8.utils.XmlUtils; 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.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import static io.fabric8.utils.DomHelper.firstChild; /** * Migrates the generated Kubernetes manifest to be used by the 3.x or later of the fabric8-maven-plugin */ @Mojo(name = "migrate", requiresDependencyResolution = ResolutionScope.RUNTIME, defaultPhase = LifecyclePhase.INSTALL) public class MigrateMojo extends AbstractFabric8Mojo { /** * The output folder for the migrations */ @Parameter(property = "fabric8.migrate.outputDir", defaultValue = "${basedir}/src/main/fabric8") protected File outputDir; private Map<String, String> kindAliases = new HashMap(); /** * Should we also update the pom.xml to remove the fabric8-maven-plugin 2.x properties? */ @Parameter(property = "fabric8.migrate.outputDir", defaultValue = "true") private boolean updatePom; /** * Should we ensure that the fabric8-maven-plugin has executions? If using a multi module project * you may wish to define these executions in the parent pom */ @Parameter(property = "fabric8.migrate.updateExecutions", defaultValue = "true") private boolean updateExecutions; @Override public void execute() throws MojoExecutionException, MojoFailureException { init(); File json = getKubernetesJson(); if (Files.isFile(json)) { try { Object dto = KubernetesHelper.loadJson(json); if (dto == null) { throw new MojoFailureException("Cannot load kubernetes json: " + json); } Set<HasMetadata> entities = new TreeSet<>(new HasMetadataComparator()); ConfigMap parameterConfigMap = null; if (dto instanceof Template) { Template template = (Template) dto; List<HasMetadata> objects = template.getObjects(); if (objects != null) { entities.addAll(objects); } List<io.fabric8.openshift.api.model.Parameter> parameters = template.getParameters(); if (parameters != null && parameters.size() > 0) { JsonSchema schema = new JsonSchema(); Map<String, String> configMapData = new TreeMap<>(); for (io.fabric8.openshift.api.model.Parameter parameter : parameters) { String name = parameter.getName(); String value = parameter.getValue(); if (value == null) { value = ""; } String key = convertToConfigMapKey(name); JsonSchemaProperty property = schema.getOrCreateProperty(key); String generate = parameter.getGenerate(); if (Strings.isNotBlank(generate)) { property.setGenerate(generate); } Boolean required = parameter.getRequired(); if (required != null && required.booleanValue()) { schema.addRequired(name); } String description = parameter.getDescription(); if (Strings.isNotBlank(description)) { property.setDescription(description); } if (Strings.isNotBlank(value)) { property.setDefaultValue(value); } configMapData.put(key, value); } String jsonSchemaJson = KubernetesHelper.toPrettyJson(schema); getLog().info("Generated ConfigMap JSON Schema: " + jsonSchemaJson); parameterConfigMap = new ConfigMapBuilder().withNewMetadataLike(template.getMetadata()). addToAnnotations(Annotations.Config.JSON_SCHEMA, jsonSchemaJson).endMetadata().build(); parameterConfigMap.setData(configMapData); migrateEntity(parameterConfigMap, parameterConfigMap); entities.add(parameterConfigMap); } } else { entities.addAll(KubernetesHelper.toItemList(dto)); } outputDir.mkdirs(); for (HasMetadata entity : entities) { entity = migrateEntity(entity, parameterConfigMap); String name = KubernetesHelper.getName(entity); String kind = shortenKind(KubernetesHelper.getKind(entity).toLowerCase()); File outFile = new File(outputDir, name + "-" + kind + ".yml"); if (entity instanceof ConfigMap || entity instanceof Secret) { KubernetesHelper.saveYaml(entity, outFile); } else { KubernetesHelper.saveYamlNotEmpty(entity, outFile); } getLog().info("Generated migration file: " + outFile); } tryAddFilesToGit("."); File basedir = getProject().getBasedir(); if (updatePom) { updatePomFile(new File(basedir, "pom.xml")); } String[] filesToDelete = {"uses.fmp2", "src/main/fabric8/templateParameters.properties", "src/main/fabric8/env.properties", "src/main/fabric8/kubernetes.json"}; for (String fileName : filesToDelete) { File file = new File(basedir, fileName); if (file.exists()) { file.delete(); } } deleteModelProcessorJavaFiles(new File(basedir, "src/main/java")); } catch (Exception e) { throw new MojoExecutionException(e.getMessage(), e); } } } private void deleteModelProcessorJavaFiles(File dir) { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile()) { String name = file.getName().toLowerCase(); if (name.endsWith(".java")) { try { String text = IOHelpers.readFully(file); if (text.contains("@KubernetesModelProcessor")) { file.delete(); } } catch (IOException e) { getLog().warn("Failed to load file " + file + ". " + e, e); } } } else if (file.isDirectory()) { deleteModelProcessorJavaFiles(file); } } } } private void tryAddFilesToGit(String filePattern) { FileRepositoryBuilder builder = new FileRepositoryBuilder(); try { Repository repository = builder .readEnvironment() // scan environment GIT_* variables .findGitDir() // scan up the file system tree .build(); Git git = new Git(repository); git.add().addFilepattern(filePattern).call(); } catch (Exception e) { getLog().warn("Failed to add generated files to the git repository: " + e, e); } } protected void updatePomFile(File pom) throws MojoExecutionException { boolean updated = false; Document doc; try { doc = XmlUtils.parseDoc(pom); } catch (Exception e) { getLog().error("Failed to parse pom " + pom + ". " + e, e); throw new MojoExecutionException(e.getMessage(), e); } Map<String, String> propertyMap = new HashMap<>(); Element properties = firstChild(doc.getDocumentElement(), "properties"); if (properties != null) { NodeList childNodes = properties.getChildNodes(); if (childNodes != null) { boolean lastRemoved = false; for (int i = 0; i < childNodes.getLength(); i++) { Node item = childNodes.item(i); if (item instanceof Element) { Element property = (Element) item; String tagName = property.getTagName(); String value = property.getTextContent(); propertyMap.put(tagName, value); if (removePropertyName(tagName, value)) { properties.removeChild(property); i--; lastRemoved = true; updated = true; } } else if (item instanceof Text) { Text text = (Text) item; if (lastRemoved) { properties.removeChild(text); i--; lastRemoved = false; } } } } } if (removeProfiles(doc, "docker-build", "docker-push", "jube")) { updated = true; } if (removePlugin(doc, "io.fabric8.jube", "jube-maven-plugin")) { updated = true; } if (migrateDockerMavenPluginConfiguration(doc, propertyMap)) { updated = true; } if (updated) { getLog().info("Updating the pom " + pom); try { DomHelper.save(doc, pom); } catch (Exception e) { getLog().error("Failed to update pom " + pom + ". " + e, e); throw new MojoExecutionException(e.getMessage(), e); } } } private boolean removeProfiles(Document doc, String... profileIds) { Set<String> profileIdSet = new HashSet<>(Arrays.asList(profileIds)); boolean updated = false; Element profiles = firstChild(doc.getDocumentElement(), "profiles"); if (profiles != null) { NodeList childNodes = profiles.getChildNodes(); if (childNodes != null) { boolean lastRemoved = false; for (int i = 0; i < childNodes.getLength(); i++) { Node item = childNodes.item(i); if (item instanceof Element) { Element profile = (Element) item; Element idElement = firstChild(profile, "id"); if (idElement != null) { String id = idElement.getTextContent(); if (id != null && profileIdSet.contains(id)) { profiles.removeChild(profile); i--; lastRemoved = true; updated = true; } } } else if (item instanceof Text) { Text text = (Text) item; if (lastRemoved) { profiles.removeChild(text); i--; lastRemoved = false; } } } } } return updated; } private boolean migrateDockerMavenPluginConfiguration(Document doc, Map<String, String> propertyMap) { boolean updated = false; Element configuration = null; Element dmpPlugin = findPlugin(doc, "io.fabric8", "docker-maven-plugin"); if (dmpPlugin != null) { configuration = firstChild(dmpPlugin, "configuration"); if (configuration != null) { DomHelper.detach(configuration); } Node nextSibling = dmpPlugin.getNextSibling(); if (nextSibling instanceof TextNode) { DomHelper.detach(nextSibling); } DomHelper.detach(dmpPlugin); updated = true; } Element fmpPlugin = findPlugin(doc, "io.fabric8", "fabric8-maven-plugin"); if (fmpPlugin == null) { if (configuration != null) { fmpPlugin = findOrAddPlugin(doc, "io.fabric8", "fabric8-maven-plugin", "${fabric8.maven.plugin.version}", configuration); updated = true; } } else { if (configuration != null) { Element oldConfig = firstChild(fmpPlugin, "configuration"); DomHelper.detach(oldConfig); fmpPlugin.appendChild(configuration); if (oldConfig == null) { fmpPlugin.appendChild(doc.createTextNode("\n ")); } } } if (updateExecutions && fmpPlugin != null) { Element executions = firstChild(fmpPlugin, "executions"); if (executions == null) { executions = DomHelper.addChildElement(fmpPlugin, "executions"); fmpPlugin.appendChild(doc.createTextNode("\n ")); } else { // lets remove all the children to be sure DomHelper.removeChildren(executions); } executions.appendChild(doc.createTextNode("\n ")); Element execution = DomHelper.addChildElement(executions, "execution"); execution.appendChild(doc.createTextNode("\n ")); DomHelper.addChildElement(execution, "id", "fmp"); execution.appendChild(doc.createTextNode("\n ")); Element goals = DomHelper.addChildElement(execution, "goals"); execution.appendChild(doc.createTextNode("\n ")); String[] goalNames = {"resource", "helm", "build"}; for (String goalName : goalNames) { goals.appendChild(doc.createTextNode("\n ")); DomHelper.addChildElement(goals, "goal", goalName); } goals.appendChild(doc.createTextNode("\n ")); executions.appendChild(doc.createTextNode("\n ")); updated= true; } return updated; } private boolean removePlugin(Document doc, String groupId, String artifactId) { Element plugin = findPlugin(doc, groupId, artifactId); if (plugin != null) { Node nextSibling = plugin.getNextSibling(); DomHelper.detach(plugin); if (nextSibling instanceof TextNode) { DomHelper.detach(nextSibling); } return true; } return false; } private Element findOrAddPlugin(Document doc, String groupId, String artifactId, String version, Element configuration) { Element plugin = findPlugin(doc, groupId, artifactId); if (plugin != null) { return plugin; } Element documentElement = doc.getDocumentElement(); Element build = firstChild(documentElement, "build"); if (build == null) { build = DomHelper.addChildElement(documentElement, "build"); } Element plugins = firstChild(build, "plugins"); if (plugins == null) { plugins = DomHelper.addChildElement(build, "plugins"); } plugins.appendChild(doc.createTextNode("\n ")); plugin = DomHelper.addChildElement(plugins, "plugin"); plugin.appendChild(doc.createTextNode("\n ")); DomHelper.addChildElement(plugin, "groupId", groupId); plugin.appendChild(doc.createTextNode("\n ")); DomHelper.addChildElement(plugin, "artifactId", artifactId); plugin.appendChild(doc.createTextNode("\n ")); DomHelper.addChildElement(plugin, "version", version); plugin.appendChild(doc.createTextNode("\n ")); if (configuration != null) { plugin.appendChild(configuration); } plugin.appendChild(doc.createTextNode("\n ")); plugins.appendChild(doc.createTextNode("\n ")); return plugin; } private Element findPlugin(Document doc, String groupId, String artifactId) { Element build = firstChild(doc.getDocumentElement(), "build"); if (build != null) { Element plugins = firstChild(build, "plugins"); if (plugins != null) { NodeList childNodes = plugins.getChildNodes(); if (childNodes != null) { for (int i = 0; i < childNodes.getLength(); i++) { Node item = childNodes.item(i); if (item instanceof Element) { Element plugin = (Element) item; if (Objects.equals(DomHelper.firstChildTextContent(plugin, "groupId"), groupId) && Objects.equals(DomHelper.firstChildTextContent(plugin, "artifactId"), artifactId)) { return plugin; } } } } } } return null; } private boolean removePropertyName(String tagName, String value) { return tagName.startsWith("docker.port.") || tagName.startsWith("fabric8.") || tagName.equals("docker.maven.plugin.version") || tagName.equals("jube.version"); } protected String convertToConfigMapKey(String name) { return name.toLowerCase().replace('_', '-'); } /** * Returns the migrated entity * <p> * - use Deployment by default instead of ReplicationController * - remove some annotations which should be generated at real build time * - replace groupId, artifactId and version with expressions */ protected HasMetadata migrateEntity(HasMetadata entity, ConfigMap parameterConfigMap) { migrateMetadata(entity.getMetadata()); if (entity instanceof ReplicationController) { ReplicationController rc = (ReplicationController) entity; ReplicationControllerSpec rcSpec = rc.getSpec(); Deployment deployment = new Deployment(); deployment.setMetadata(entity.getMetadata()); if (rcSpec != null) { DeploymentSpec deploymentSpec = new DeploymentSpec(); Map<String, String> selector = rcSpec.getSelector(); if (selector != null) { selector = new LinkedHashMap<>(selector); // Deployment's selector should not have a version as we use that for each RC / RS selector.remove("version"); } migrateLabels(selector); deploymentSpec.setReplicas(rcSpec.getReplicas()); if (selector != null) { deploymentSpec.setSelector(new LabelSelectorBuilder().withMatchLabels(selector).build()); } PodTemplateSpec podTemplateSpec = rcSpec.getTemplate(); if (podTemplateSpec != null) { PodSpec podSpec = podTemplateSpec.getSpec(); if (podSpec != null) { List<Container> containers = podSpec.getContainers(); if (containers != null) { for (Container container : containers) { migrateContainer(container, parameterConfigMap); } } } } PodTemplateSpec template = rcSpec.getTemplate(); if (template != null) { migrateMetadata(template.getMetadata()); deploymentSpec.setTemplate(template); } deployment.setSpec(deploymentSpec); } return deployment; } return entity; } protected void migrateContainer(Container container, ConfigMap parameterConfigMap) { String image = container.getImage(); if (image != null) { MavenProject project = getProject(); String version = project.getVersion(); if (version != null) { String label = ":" + version; if (image.endsWith(label)) { image = Strings.stripSuffix(image, label) + ":${project.version}"; container.setImage(image); } else { getLog().warn("Image does not end with " + label + " as image is: " + image); } } } if (parameterConfigMap != null) { Map<String, String> parameters = parameterConfigMap.getData(); if (parameters != null) { String configMapName = KubernetesHelper.getName(parameterConfigMap); List<EnvVar> env = container.getEnv(); for (EnvVar envVar : env) { String value = envVar.getValue(); if (value != null) { String name = envVar.getName(); if (value.startsWith("${") && value.endsWith("}")) { String variableName = Strings.stripPrefix(Strings.stripSuffix(name, "}"), "${"); String expression = convertToConfigMapKey(variableName); if (parameters.containsKey(expression)) { // lets switch to a refer to the config map! envVar.setValue(null); envVar.setValueFrom(new EnvVarSourceBuilder(). withNewConfigMapKeyRef().withName(configMapName).withKey(expression).endConfigMapKeyRef().build()); } } } } } } } protected void migrateMetadata(ObjectMeta metadata) { if (metadata == null) { return; } Map<String, String> annotations = metadata.getAnnotations(); if (annotations != null) { annotations.remove(Annotations.Builds.BUILD_ID); annotations.remove(Annotations.Builds.BUILD_URL); annotations.remove(Annotations.Builds.GIT_BRANCH); annotations.remove(Annotations.Builds.GIT_COMMIT); annotations.remove(Annotations.Builds.GIT_URL); } migrateLabels(metadata.getLabels()); } private void migrateLabels(Map<String, String> labels) { if (labels != null) { MavenProject project = getProject(); // TODO there is a currently a bug when using values other than ${project.artifactId} in fabric8-maven-plugin 3.x // where it overrides the value of the sepc.selector.matchLabels to the artifactId which then breaks // as the template.metadata.labels.project will differ boolean alwaysUseArtifactIdForProject = true; if (alwaysUseArtifactIdForProject) { if (labels.containsKey("project")) { labels.put("project", "${project.artifactId}"); } } else { replaceKeyValueWith(labels, "project", project.getArtifactId(), "${project.artifactId}"); } replaceKeyValueWith(labels, "version", project.getVersion(), "${project.version}"); } } private void replaceKeyValueWith(Map<String, String> labels, String key, String oldValue, String newValue) { String value = labels.get(key); if (Objects.equals(value, oldValue)) { labels.put(key, newValue); } } private void init() { kindAliases.put("configmap", "cm"); kindAliases.put("deploymentconfig", "dc"); kindAliases.put("replicationcontroller", "rc"); kindAliases.put("replicaset", "rs"); kindAliases.put("service", "svc"); kindAliases.put("serviceaccount", "sa"); } private String shortenKind(String kind) { String answer = kindAliases.get(kind); if (answer == null) { return kind; } return answer; } }