From 99859399d703737b7aea128c3f061f9af466ecdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Mass=C3=A9?= Date: Tue, 21 May 2019 17:33:50 +0200 Subject: [PATCH] Work in progress --- 3scale_toolbox.groovy | 296 +++++++++++++++++++++++ README.md | 13 + testcase-01/Jenkinsfile | 32 +++ testcase-01/setup.yaml | 34 +++ swagger.json => testcase-01/swagger.json | 0 5 files changed, 375 insertions(+) create mode 100644 3scale_toolbox.groovy create mode 100644 README.md create mode 100644 testcase-01/Jenkinsfile create mode 100644 testcase-01/setup.yaml rename swagger.json => testcase-01/swagger.json (100%) diff --git a/3scale_toolbox.groovy b/3scale_toolbox.groovy new file mode 100644 index 0000000..79bf236 --- /dev/null +++ b/3scale_toolbox.groovy @@ -0,0 +1,296 @@ +#!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; diff --git a/README.md b/README.md new file mode 100644 index 0000000..b642220 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# API Lifecycle Mockup + +## Setup + +```sh +oc project api-lifecycle +3scale remote add $NAME https://$TOKEN@$TENANT.3scale.net/ +oc create secret generic 3scale-toolbox --from-file=~/.3scalerc.yaml +``` + +```sh +oc process -f testcase-01/setup.yaml |oc create -f - +``` diff --git a/testcase-01/Jenkinsfile b/testcase-01/Jenkinsfile new file mode 100644 index 0000000..236a0c2 --- /dev/null +++ b/testcase-01/Jenkinsfile @@ -0,0 +1,32 @@ +#!groovy + +def toolbox = load '../3scale_toolbox.groovy' +def toolboxConfig = [ + "secretName": params.SECRET_NAME +] +def baseSystemName = toolbox.generateRandomBaseSystemName() + +node() { + stage('Checkout Source') { + checkout scm + } + + stage("Get toolbox version") { + openshift.withCluster() { + openshift.withProject(params.NAMESPACE) { + echo "toolbox version = " + toolbox.getToolboxVersion() + } + } + } + + stage("Import OpenAPI") { + openshift.withCluster() { + openshift.withProject(params.NAMESPACE) { + toolbox.importOpenAPI(destination: params.TARGET_INSTANCE, + toolboxConfig: toolboxConfig, + oasFile: "swagger.json", + baseSystemName: baseSystemName) + } + } + } +} diff --git a/testcase-01/setup.yaml b/testcase-01/setup.yaml new file mode 100644 index 0000000..44d2642 --- /dev/null +++ b/testcase-01/setup.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: Template +metadata: + name: testcase-01 +objects: +- kind: "BuildConfig" + apiVersion: "v1" + metadata: + name: "testcase-01" + namespace: ${NAMESPACE} + spec: + source: + git: + uri: ${GIT_REPO} + strategy: + type: "JenkinsPipeline" + jenkinsPipelineStrategy: + jenkinsfilePath: testcase-01/Jenkinsfile + env: + - name: SECRET_NAME + value: ${SECRET_NAME} + - name: NAMESPACE + value: ${NAMESPACE} + - name: TARGET_INSTANCE + value: ${TARGET_INSTANCE} +parameters: +- name: SECRET_NAME + value: 3scale-toolbox +- name: NAMESPACE + value: api-lifecycle +- name: TARGET_INSTANCE + value: nmasse-redhat +- name: GIT_REPO + value: https://github.com/nmasse-itix/API-Lifecycle-Mockup.git diff --git a/swagger.json b/testcase-01/swagger.json similarity index 100% rename from swagger.json rename to testcase-01/swagger.json