blob: a76957888020ea4f45a13edeac2bdbc29c4eda61 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 org.apache.brooklyn.tasks.kubectl;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.brooklyn.core.mgmt.ha.BrooklynBomOsgiArchiveInstaller;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
/**
* This was needed to ensure our Kubernetes Yaml Job configurations are valid.
*/
public class KubeJobFileCreator {
private static final Logger LOG = LoggerFactory.getLogger(KubeJobFileCreator.class);
String jobName;
String imageName;
String imagePullPolicy;
String workingDir;
String prefix = "brooklyn-job";
List<String> command = Lists.newArrayList();
List<String> args = Lists.newArrayList();
Map<String, String> env = Maps.newHashMap();
List<Map<String,String>> volumeMounts = Lists.newArrayList();
List<Map<String, Object>> volumes = Lists.newArrayList();
public KubeJobFileCreator withName(final String name) {
this.jobName = name;
return this;
}
public KubeJobFileCreator withImage(final String image){
this.imageName = image;
return this;
}
/**
* If {@code imagePullPolicy} is not set for a container, Kubernetes defaults to Always.
* @param eimagePullPolicy
* @return
*/
public KubeJobFileCreator withImagePullPolicy(final PullPolicy eimagePullPolicy){
if (eimagePullPolicy != null) {
this.imagePullPolicy = eimagePullPolicy.val();
}
return this;
}
public KubeJobFileCreator withCommand(final List<String> commandAndEntryPointArgs){
if (commandAndEntryPointArgs != null) {
this.command.addAll(commandAndEntryPointArgs);
}
return this;
}
public KubeJobFileCreator withArgs(final List<String> args){
if (args != null) {
this.args.addAll(args);
}
return this;
}
public KubeJobFileCreator withVolumeMounts(final Set<Map<String,String>> volumeMounts) {
if (volumeMounts != null) {
this.volumeMounts.addAll(volumeMounts);
}
return this;
}
public KubeJobFileCreator withVolumes(final Set<Map<String, Object>> volumes) {
if (volumes != null) {
this.volumes.addAll(volumes);
}
return this;
}
public KubeJobFileCreator withWorkingDir(String workingDir) {
this.workingDir = workingDir;
return this;
}
public KubeJobFileCreator withPrefix(final String prefixArg){
this.prefix = prefixArg;
return this;
}
public KubeJobFileCreator withEnv(final Map<String,String> env){
if (env != null) {
this.env.putAll(env);
}
return this;
}
public BrooklynBomOsgiArchiveInstaller.FileWithTempInfo<File> createFile(){
JobTemplate jobTemplate = buildJobTemplate();
return serializeAndWriteToTempFile(jobTemplate);
}
public String getAsString(){
JobTemplate jobTemplate = buildJobTemplate();
StringWriter sw = new StringWriter();
serializeAndWriteToWriter(jobTemplate, sw);
return sw.toString();
}
private JobTemplate buildJobTemplate() {
JobTemplate jobTemplate = new JobTemplate(jobName);
ContainerSpec containerSpec = jobTemplate.getSpec().getTemplate().getContainerSpec(0);
if(Strings.isNonBlank(workingDir)) {
containerSpec.setWorkingDir(workingDir);
}
containerSpec.setImage(imageName);
containerSpec.setImagePullPolicy(imagePullPolicy);
if (!env.isEmpty()) {
List<Map<String,String>> envList = env.entrySet().stream().map (e -> {
Map<String,String> envItem = new HashMap<>();
envItem.put("name", e.getKey());
envItem.put("value", e.getValue());
return envItem;
}).collect(Collectors.toList());
containerSpec.setEnv(envList);
}
if (!command.isEmpty()) {
containerSpec.setCommand(this.command);
}
if (!args.isEmpty()) {
containerSpec.setArgs(this.args);
}
final Set<String> volumeNames = new HashSet<>();
if (!volumes.isEmpty()) {
jobTemplate.getSpec().getTemplate().getSpec().setVolumes(volumes);
volumes.stream().map(volumeSpec -> (String)volumeSpec.get("name")).forEach(volumeNames::add);
}
if (!volumeMounts.isEmpty()) {
List<VolumeMount> vms = Lists.newArrayList();
volumeMounts.forEach(vmMap -> {
VolumeMount vm = new VolumeMount();
vm.setName(vmMap.get("name"));
if(!volumeNames.contains(vm.getName())) {
throw new IllegalArgumentException("The Job " + this.jobName + "is invalid: spec.template.spec.containers[0].volumeMounts[0].name: Not found:\"" + vm.getName() + "\"");
}
vm.setMountPath(vmMap.get("mountPath"));
vms.add(vm);
});
containerSpec.setVolumeMounts(vms);
}
return jobTemplate;
}
private BrooklynBomOsgiArchiveInstaller.FileWithTempInfo<File> serializeAndWriteToTempFile(JobTemplate jobTemplate) {
try {
File jobBodyPath = File.createTempFile(prefix, ".yaml");
jobBodyPath.deleteOnExit(); // We should have already deleted it, but just in case
serializeAndWriteToWriter(jobTemplate, new PrintWriter(jobBodyPath));
LOG.debug("Job body dumped at: {}", jobBodyPath.getAbsolutePath());
return new BrooklynBomOsgiArchiveInstaller.FileWithTempInfo<>(jobBodyPath, true);
} catch (IOException e) {
throw new RuntimeException("Failed to create temp file for container", e);
}
}
private void serializeAndWriteToWriter(JobTemplate jobTemplate, Writer writer) {
DumperOptions options = new DumperOptions();
options.setIndent(2);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Representer representer = new Representer(options){
@Override
protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
// if value of property is null, ignore it.
if (propertyValue == null) {
return null;
}
else {
return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
}
}
};
representer.addClassTag(JobTemplate.class, Tag.MAP);
try {
File jobBodyPath = File.createTempFile(prefix, ".yaml");
jobBodyPath.deleteOnExit(); // We should have already deleted it, but just in case
Yaml yaml = new Yaml(representer, options);
yaml.dump(jobTemplate, writer);
} catch (IOException e) {
throw new RuntimeException("Failed to write job file for container", e);
}
}
}
/**
* Type mapping to the value of the {@code spec} element
*/
class TemplateSpec {
/**
* As pods successfully complete, the Job tracks the successful completions. When a specified number of successful completions is reached, the task (ie, Job) is complete.
* Note that even if you specify .spec.parallelism = 1 and .spec.completions = 1 and .spec.template.spec.restartPolicy = "Never", the same program may sometimes be started twice.
*/
Integer completions = 1;
Integer parallelism = 1;
/**
* To do so, set .spec.backoffLimit to specify the number of retries before considering a Job as failed. The back-off limit is set by default to 6.
*/
Integer backoffLimit = 0;
JobSpec template;
public TemplateSpec() {
template = new JobSpec();
}
public Integer getCompletions() {
return completions;
}
public void setCompletions(Integer completions) {
this.completions = completions;
}
public Integer getParallelism() {
return parallelism;
}
public void setParallelism(Integer parallelism) {
this.parallelism = parallelism;
}
public JobSpec getTemplate() {
return template;
}
public void setTemplate(JobSpec template) {
this.template = template;
}
public Integer getBackoffLimit() {
return backoffLimit;
}
public void setBackoffLimit(Integer backoffLimit) {
this.backoffLimit = backoffLimit;
}
}
/**
* Matches the root of the yaml file
*/
class JobTemplate {
String kind = "Job";
String apiVersion = "batch/v1";
Map<String, String> metadata;
TemplateSpec spec;
public JobTemplate() {
}
public JobTemplate(String name) {
metadata = Maps.newHashMap();
metadata.put("name", name);
spec = new TemplateSpec();
}
public String getApiVersion() {
return apiVersion;
}
// Do not explicitly call this
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
// Do not explicitly call this
public void setKind(String kind) {
this.kind = kind;
}
public String getKind() {
return kind;
}
public Map<String, String> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, String> metadata) {
this.metadata = metadata;
}
public TemplateSpec getSpec() {
return spec;
}
public void setSpec(TemplateSpec spec) {
this.spec = spec;
}
}
/**
* Type mapping to the value of the {@code template} element
*/
class JobSpec {
ContainerSpecs spec;
public JobSpec() {
this.spec = new ContainerSpecs();
}
public ContainerSpecs getSpec() {
return spec;
}
public void setSpec(ContainerSpecs spec) {
this.spec = spec;
}
public ContainerSpec getContainerSpec(int index) {
if(this.spec.containers.size() > 0) {
return this.spec.containers.get(index);
}
return null;
}
}
/**
* Type mapping to the value of the {@code template.spec} element
*/
class ContainerSpecs {
List<ContainerSpec> containers;
List<Map<String, Object>> volumes;
Boolean automountServiceAccountToken = false;
String restartPolicy = "Never";
public ContainerSpecs() {
this.containers = Lists.newArrayList();
this.containers.add(new ContainerSpec());}
public List<ContainerSpec> getContainers() {
return containers;
}
public void setContainers(List<ContainerSpec> containers) {
this.containers = containers;
}
public String getRestartPolicy() {
return restartPolicy;
}
public void setRestartPolicy(String restartPolicy) {
this.restartPolicy = restartPolicy;
}
public Boolean getAutomountServiceAccountToken() {
return automountServiceAccountToken;
}
public void setAutomountServiceAccountToken(Boolean automountServiceAccountToken) {
this.automountServiceAccountToken = automountServiceAccountToken;
}
public List<Map<String, Object>> getVolumes() {
return volumes;
}
public void setVolumes(List<Map<String, Object>> volumes) {
this.volumes = volumes;
}
}
/**
* Type mapping to the value of the {@code template.spec.containers} element
*/
class ContainerSpec {
String name = "test";
String image = "defaultImage";
String imagePullPolicy = "IfNotPresent";
String workingDir = null; // default is /
List<String> command = null;
List<String> args = null;
List<VolumeMount> volumeMounts = null;
List<Map<String, String>> env = null;
public ContainerSpec() {
}
public String getName() {
return name;
}
// Do not explicitly call this
public void setName(String name) {
this.name = name;
}
public String getImage() {
return image;
}
public String getImagePullPolicy() {
return imagePullPolicy;
}
public void setImagePullPolicy(String imagePullPolicy) {
this.imagePullPolicy = imagePullPolicy;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getCommand() {
return command;
}
public void setCommand(List<String> command) {
this.command = command;
}
public List<String> getArgs() {
return args;
}
public void setArgs(List<String> args) {
this.args = args;
}
public List<Map<String, String>> getEnv() {
return env;
}
public void setEnv(List<Map<String, String>> env) {
this.env = env;
}
public void setVolumeMounts(List<VolumeMount> volumeMounts) {
this.volumeMounts = volumeMounts;
}
public List<VolumeMount> getVolumeMounts() {
return volumeMounts;
}
public String getWorkingDir() {
return workingDir;
}
public void setWorkingDir(String workingDir) {
this.workingDir = workingDir;
}
}
/**
* Type mapping to the value of the {@code template.spec.containers.volumeMounts} element
*/
class VolumeMount {
String name;
String mountPath;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMountPath() {
return mountPath;
}
public void setMountPath(String mountPath) {
this.mountPath = mountPath;
}
}