A mockup of what an automated API Lifecycle could look like
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

296 lines
9.0 KiB

#!groovy
def importOpenAPI(Map conf) {
assert conf.destination != null
assert conf.baseSystemName != null
assert conf.oasFile != null
// Read the OpenAPI Specification file
def openAPI = readOpenAPISpecificationFile(conf.oasFile)
assert openAPI.swagger == "2.0"
def version = openAPI.info.version
assert version != null
def major = version.tokenize(".")[0]
def baseName = basename(conf.oasFile)
// Compute the target system_name
def targetSystemName = (conf.environmentName != null ? "${conf.environmentName}_" : "") + conf.baseSystemName + "_${major}"
def commandLine = "3scale import openapi -t ${targetSystemName} -d ${conf.destination} /artifacts/${baseName}"
def result = runToolbox(commandLine: commandLine,
jobName: "import",
openAPI: [
"filename": baseName,
"content": readFile(conf.oasFile)
],
toolboxConfig: conf.toolboxConfig)
echo result.stdout
}
def basename(path) {
return path.drop(path.lastIndexOf("/") != -1 ? path.lastIndexOf("/") : 0)
}
def readOpenAPISpecificationFile(fileName) {
if (fileName.toLowerCase().endsWith(".json")) {
return readJSON(file: fileName)
} else if (fileName.toLowerCase().endsWith(".yaml") || fileName.toLowerCase().endsWith(".yml")) {
return readYaml(file: fileName)
} else {
throw new Exception("Can't decide between JSON and YAML on ${fileName}")
}
}
def getToolboxVersion() {
def result = runToolbox(commandLine: "3scale -v",
jobName: "version")
return result.stdout
}
def generateRandomBaseSystemName() {
String alphabet = (('A'..'N')+('P'..'Z')+('a'..'k')+('m'..'z')+('2'..'9')).join()
def length = 6
id = new Random().with {
(1..length).collect{ alphabet[nextInt(alphabet.length())] }.join()
}
return "testcase_${id}"
}
def runToolbox(Map conf) {
def result = null
assert conf.jobName != null
assert conf.commandLine != null
def defaultToolboxConf = [
"toolboxConfig": null,
"openAPI": null,
"image": "quay.io/redhat/3scale-toolbox:v0.10.0",
"backoffLimit": 2, // three attempts (one first try + two retries)
"imagePullPolicy": "IfNotPresent",
"activeDeadlineSeconds": 90
]
// Apply default values
conf = defaultToolboxConf + conf
if (conf.toolboxConfig != null && conf.toolboxConfig.configFileId != null) {
// Generate a default secret name if none has been provided
if (conf.toolboxConfig.secretName == null) {
conf.toolboxConfig = [
"configFileId": conf.toolboxConfig.configFileId,
"secretName": "3scale-toolbox-${JOB_BASE_NAME}"
]
}
echo "Creating a secret named ${conf.toolboxConfig.secretName} containing file ${conf.toolboxConfig.configFileId}..."
configFileProvider([configFile(fileId: conf.toolboxConfig.configFileId, variable: 'TOOLBOX_CONFIG')]) {
def toolboxConfig = readFile(TOOLBOX_CONFIG)
createSecret(conf.toolboxConfig.secretName, [ ".3scalerc.yaml": toolboxConfig ])
}
}
def oasConfigMapName = null
if (conf.openAPI != null) {
oasConfigMapName = "3scale-toolbox-${JOB_BASE_NAME}-${BUILD_NUMBER}-openapi"
echo "Creating a configMap named ${oasConfigMapName} containing the OpenAPI file..."
createConfigMap(oasConfigMapName, [ (conf.openAPI.filename): conf.openAPI.content ])
}
def jobName = "${JOB_BASE_NAME}-${BUILD_NUMBER}-${conf.jobName}"
def jobSpecs = [
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": [
"name": jobName,
"labels": [
"build": "${JOB_BASE_NAME}-${BUILD_NUMBER}",
"job": "${JOB_BASE_NAME}"
]
],
"spec": [
"backoffLimit": conf.backoffLimit,
"activeDeadlineSeconds": conf.activeDeadlineSeconds,
"template": [
"spec": [
"restartPolicy": "Never",
"containers": [
[
"name": "job",
"image": conf.image,
"imagePullPolicy": conf.imagePullPolicy,
"command": [ "scl", "enable", "rh-ruby25", "/opt/rh/rh-ruby25/root/usr/local/bin/${conf.commandLine}" ],
"volumeMounts": [
[
"mountPath": "/opt/app-root/src/",
"name": "toolbox-config"
],
[
"mountPath": "/artifacts",
"name": "artifacts"
]
]
]
],
"volumes": [
[
"name": "toolbox-config"
],
[
"name": "artifacts"
]
]
]
]
]
]
// Inject the toolbox configuration as a volume
if (conf.toolboxConfig != null && conf.toolboxConfig.secretName != null) {
jobSpecs.spec.template.spec.volumes[0].secret = [
"secretName": conf.toolboxConfig.secretName
]
} else {
jobSpecs.spec.template.spec.volumes[0].emptyDir = [:]
}
// Inject the OpenAPI file as a volume
if (oasConfigMapName != null) {
jobSpecs.spec.template.spec.volumes[1].configMap = [
"name": oasConfigMapName
]
} else {
jobSpecs.spec.template.spec.volumes[1].emptyDir = [:]
}
def job = null
try {
job = openshift.create(jobSpecs)
int jobTimeout = 2 + (int)(conf.activeDeadlineSeconds / 60.0f)
echo "Waiting ${jobTimeout} minutes for the job to complete..."
timeout(jobTimeout) {
// Wait for the job to complete, either Succeeded or Failed
job.watch {
def jobStatus = getJobStatus(it.object())
echo "Job ${it.name()}: succeeded = ${jobStatus.succeeded}, failed = ${jobStatus.failed}, status = ${jobStatus.status}, reason = ${jobStatus.reason}"
// Exit the watch loop when the Job has one successful pod or failed
return jobStatus.succeeded > 0 || jobStatus.status == "Failed"
}
}
} finally {
if (job != null) {
def jobStatus = getJobStatus(job.object())
echo "job ${job.name()} has status '${jobStatus.status}' and reason '${jobStatus.reason}'"
// Iterate over pods to find:
// - the pod that succeeded
// - as last resort, a pod that failed
def pods = job.related("pod")
pods.withEach {
if (it.object().status.phase == "Succeeded") {
result = getPodDetails(it)
}
if (it.object().status.phase == "Failed" && result == null) {
result = getPodDetails(it)
}
}
if (result != null && result.podPhase == "Failed") {
echo "RC: ${result.status}"
echo "STDOUT:"
echo "-------"
echo result.stdout
echo "STDERR:"
echo "-------"
echo result.stderr
error("job ${job.name()} exited with '${jobStatus.status}' and reason '${jobStatus.reason}'")
}
// Delete the job
try {
openshift.selector('job', jobName).delete()
} catch (e2) { // Best effort
echo "cannot delete the job ${jobName}: ${e2}"
}
}
// Delete the temporary configMap containing the OAS file
if (oasConfigMapName != null) {
try {
openshift.selector('configMap', oasConfigMapName).delete()
} catch (e2) { // Best effort
echo "cannot delete the configMap ${oasConfigMapName}: ${e2}"
}
}
// Delete the temporary secret
if (conf.toolboxConfig != null && conf.toolboxConfig.configFileId != null) {
try {
openshift.selector('secret', conf.toolboxConfig.secretName).delete()
} catch (e2) { // Best effort
echo "cannot delete the secret ${conf.toolboxConfig.secretName}: ${e2}"
}
}
}
return result
}
def getJobStatus(obj) {
return [
"succeeded": obj.status.succeeded != null ? obj.status.succeeded : 0,
"failed": obj.status.failed != null ? obj.status.failed : 0,
"status": obj.status.conditions != null && obj.status.conditions.size() > 0 ? obj.status.conditions[0].type : "Unknown",
"reason": obj.status.conditions != null && obj.status.conditions.size() > 0 ? obj.status.conditions[0].reason : ""
]
}
def getPodDetails(pod) {
def logs = pod.logs()
return [
"status": logs.actions[0].status,
"stdout": logs.actions[0].out,
"stderr": logs.actions[0].err,
"podPhase": pod.object().status.phase,
"podName": pod.name()
]
}
def createConfigMap(configMapName, content) {
def configMapSpecs = [
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": [
"name": "${configMapName}",
"labels": [
"job": "${JOB_BASE_NAME}",
"build": "${JOB_BASE_NAME}-${BUILD_NUMBER}"
]
],
"data": [:]
]
content.each{ k, v -> configMapSpecs.data[k] = v }
openshift.apply(configMapSpecs)
}
def createSecret(secretName, content) {
def secretSpecs = [
"apiVersion": "v1",
"kind": "Secret",
"metadata": [
"name": "${secretName}",
"labels": [
"job": "${JOB_BASE_NAME}"
]
],
"stringData": [:]
]
content.each{ k, v -> secretSpecs.stringData[k] = v }
openshift.apply(secretSpecs)
}
// required to be loaded from a jenkins pipeline
return this;