2019-02-18 09:20:16 +02:00
import groovy.io.FileType
2019-03-27 18:21:08 +02:00
import groovy.json.JsonOutput
2019-05-20 11:16:59 +02:00
import groovy.json.JsonSlurper
2018-10-16 13:07:38 +02:00
import org.yaml.snakeyaml.Yaml
import org.codehaus.groovy.control.CompilerConfiguration
2019-01-11 16:18:51 +02:00
import com.sap.piper.GenerateDocumentation
2019-05-23 17:37:47 +02:00
import com.sap.piper.GenerateStageDocumentation
2019-05-31 09:36:12 +02:00
import com.sap.piper.DefaultValueCache
2018-10-16 13:07:38 +02:00
import java.util.regex.Matcher
2019-02-18 09:20:16 +02:00
import groovy.text.StreamingTemplateEngine
2018-10-16 13:07:38 +02:00
2019-05-15 20:52:45 +02:00
import com.sap.piper.MapUtils
2018-10-16 13:07:38 +02:00
//
2019-03-08 13:33:31 +02:00
// Collects helper functions for rendering the documentation
2018-10-16 13:07:38 +02:00
//
2018-10-26 11:48:11 +02:00
class TemplateHelper {
2018-10-16 13:07:38 +02:00
2019-05-20 11:16:59 +02:00
static createDependencyList ( Set deps ) {
2019-05-20 09:32:49 +02:00
def t = ''
2019-05-20 11:16:59 +02:00
t + = 'The step depends on the following Jenkins plugins\n\n'
2019-05-20 11:43:37 +02:00
def filteredDeps = deps . findAll { dep - > dep ! = 'UNIDENTIFIED' }
2019-05-24 15:09:21 +02:00
if ( filteredDeps . contains ( 'kubernetes' ) ) {
// The docker plugin is not detected by the tests since it is not
// handled via step call, but it is added to the environment.
// Hovever kubernetes plugin and docker plugin are closely related,
// hence adding docker if kubernetes is present.
filteredDeps . add ( 'docker' )
}
2019-05-20 11:43:37 +02:00
if ( filteredDeps . isEmpty ( ) ) {
t + = '* <none>\n'
} else {
filteredDeps
. sort ( )
2019-05-21 10:48:02 +02:00
. each { dep - > t + = "* [${dep}](https://plugins.jenkins.io/${dep})\n" }
2019-05-20 11:43:37 +02:00
}
2019-05-21 17:04:18 +02:00
if ( filteredDeps . contains ( 'kubernetes' ) ) {
t + = "\nThe kubernetes plugin is only used if running in a kubernetes environment."
}
2019-05-24 16:12:20 +02:00
t + = '' ' |
| Transitive dependencies are omitted .
|
| The list might be incomplete .
|
| Consider using the [ ppiper /jenkins-master](https:/ /cloud.docker.com/ u /ppiper/ repository /docker/ ppiper / jenkins - master )
| docker image . This images comes with preinstalled plugins .
| '' ' . stripMargin ( )
2019-05-20 09:32:49 +02:00
return t
}
2018-11-05 10:31:47 +02:00
static createParametersTable ( Map parameters ) {
2018-11-02 14:00:15 +02:00
2018-11-05 10:31:47 +02:00
def t = ''
t + = '| name | mandatory | default | possible values |\n'
t + = '|------|-----------|---------|-----------------|\n'
2018-10-16 13:07:38 +02:00
2018-11-05 10:31:47 +02:00
parameters . keySet ( ) . toSorted ( ) . each {
2018-10-16 13:07:38 +02:00
2018-11-05 10:31:47 +02:00
def props = parameters . get ( it )
2019-04-30 16:39:29 +02:00
2019-05-23 17:37:47 +02:00
def defaultValue = isComplexDefault ( props . defaultValue ) ? renderComplexDefaultValue ( props . defaultValue ) : renderSimpleDefaultValue ( props . defaultValue )
2019-04-30 16:39:29 +02:00
t + = "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${defaultValue} | ${props.value ?: ''} |\n"
2018-11-05 10:31:47 +02:00
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:31:47 +02:00
t
2018-10-26 11:48:11 +02:00
}
2018-10-16 13:07:38 +02:00
2019-04-30 16:39:29 +02:00
private static boolean isComplexDefault ( def _default ) {
if ( ! ( _default in Collection ) ) return false
if ( _default . size ( ) = = 0 ) return false
for ( def entry in _default ) {
if ( ! ( entry in Map ) ) return false
if ( ! entry . dependentParameterKey ) return false
if ( ! entry . key ) return false
}
return true
}
private static renderComplexDefaultValue ( def _default ) {
_default
. collect { "${it.dependentParameterKey}=`${it.key ?: '<empty>'}`:`${it.value ?: '<empty>'}`" }
. join ( '<br />' )
}
2019-05-23 17:37:47 +02:00
private static renderSimpleDefaultValue ( def _default ) {
if ( _default = = null ) return ''
return "`${_default}`"
}
2018-11-05 10:31:47 +02:00
static createParameterDescriptionSection ( Map parameters ) {
def t = ''
parameters . keySet ( ) . toSorted ( ) . each {
def props = parameters . get ( it )
t + = "* `${it}` - ${props.docu ?: ''}\n"
}
2018-10-16 13:07:38 +02:00
2019-01-14 10:47:23 +02:00
t . trim ( )
2018-10-16 13:07:38 +02:00
}
2019-02-18 09:20:16 +02:00
static createParametersSection ( Map parameters ) {
createParametersTable ( parameters ) + '\n' + createParameterDescriptionSection ( parameters )
}
2018-11-05 10:31:47 +02:00
static createStepConfigurationSection ( Map parameters ) {
2018-10-29 17:28:37 +02:00
2019-01-14 10:47:23 +02:00
def t = '' ' | We recommend to define values of step parameters via [ config . yml file ] ( . . / configuration . md ) .
2018-11-05 10:31:47 +02:00
|
2019-01-09 11:43:09 +02:00
| In following sections of the config . yml the configuration is possible: \ n \ n '' ' . stripMargin ( )
2018-10-29 17:28:37 +02:00
2019-02-13 15:29:05 +02:00
t + = '| parameter | general | step/stage |\n'
t + = '|-----------|---------|------------|\n'
2018-11-02 15:52:43 +02:00
2018-11-05 10:31:47 +02:00
parameters . keySet ( ) . toSorted ( ) . each {
def props = parameters . get ( it )
2019-02-13 15:29:05 +02:00
t + = "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} |\n"
2018-11-05 10:31:47 +02:00
}
2018-10-29 17:28:37 +02:00
2019-01-14 10:47:23 +02:00
t . trim ( )
2018-10-29 17:28:37 +02:00
}
2019-05-23 17:37:47 +02:00
static createStageContentSection ( Map stageDescriptions ) {
def t = 'This stage comprises following steps which are activated depending on your use-case/configuration:\n\n'
t + = '| step | step description |\n'
t + = '| ---- | ---------------- |\n'
stageDescriptions . each { step , description - >
t + = "| [${step}](../steps/${step}.md) | ${description.trim()} |\n"
}
return t
}
static createStageActivationSection ( ) {
def t = '' ' This stage will be active if any one of the following conditions is met:
* Stage configuration in [ config . yml file ] ( . . / configuration . md ) contains entries for this stage .
* Any of the conditions are met which are explained in the section [ Step Activation ] ( # step - activation ) .
'' '
return t . trim ( )
}
static createStepActivationSection ( Map configConditions ) {
if ( ! configConditions ) return 'For this stage no conditions are assigned to steps.'
def t = 'Certain steps will be activated automatically depending on following conditions:\n\n'
t + = '| step | config key | config value | file pattern |\n'
t + = '| ---- | ---------- | ------------ | ------------ |\n'
configConditions ? . each { stepName , conditions - >
t + = "| ${stepName} "
t + = "| ${renderValueList(conditions?.configKeys)} "
t + = "| ${renderValueList(mapToValueList(conditions?.config))} "
List filePatterns = [ ]
if ( conditions ? . filePattern ) filePatterns . add ( conditions ? . filePattern )
if ( conditions ? . filePatternFromConfig ) filePatterns . add ( conditions ? . filePatternFromConfig )
t + = "| ${renderValueList(filePatterns)} |\n"
}
t + = '' '
! ! ! info "Step condition details"
There are currently several conditions which can be checked . < br /> This is done in the [Init stage](init.md) of the pipeline shortly after checkout of the source code repository.<br/ >
* * Important: It will be sufficient that any one condition per step is met . * *
* ` config key ` : Checks if a defined configuration parameter is set .
* ` config value ` : Checks if a configuration parameter has a defined value .
* ` file pattern ` : Checks if files according a defined pattern exist in the project . Either the pattern is speficified direcly or it is retrieved from a configuration parameter .
! ! ! note "Overruling step activation conditions"
It is possible to overrule the automatically detected step activation status . < br / >
Just add to your stage configuration ` < stepName > : false ` , for example ` deployToKubernetes: false ` .
For details about the configuration options , please see [ Configuration of Piper ] ( . . / configuration . md ) .
'' '
return t
}
private static renderValueList ( List valueList ) {
if ( ! valueList ) return ''
if ( valueList . size ( ) > 1 ) {
List quotedList = [ ]
valueList . each { listItem - >
quotedList . add ( "-`${listItem}`" )
}
return quotedList . join ( '<br />' )
} else {
return "`${valueList[0]}`"
}
}
private static mapToValueList ( Map map ) {
List valueList = [ ]
map ? . each { key , value - >
if ( value instanceof List ) {
value . each { listItem - >
valueList . add ( "${key}: ${listItem}" )
}
} else {
valueList . add ( "${key}: ${value}" )
}
}
return valueList
}
static createStageConfigurationSection ( ) {
return 'The stage parameters need to be defined in the section `stages` of [config.yml file](../configuration.md).'
}
2018-10-16 13:07:38 +02:00
}
2018-10-26 11:48:11 +02:00
//
// Collects generic helper functions
//
class Helper {
2018-10-16 13:07:38 +02:00
2019-03-08 13:33:31 +02:00
static projectRoot = new File ( Helper . class . protectionDomain . codeSource . location . path ) . getParentFile ( ) . getParentFile ( ) . getParentFile ( )
2018-11-05 10:51:28 +02:00
static getConfigHelper ( classLoader , roots , script ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def compilerConfig = new CompilerConfiguration ( )
2019-05-08 11:36:01 +02:00
compilerConfig . setClasspathList ( roots )
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
new GroovyClassLoader ( classLoader , compilerConfig , true )
2019-03-08 13:33:31 +02:00
. parseClass ( new File ( projectRoot , 'src/com/sap/piper/ConfigurationHelper.groovy' ) )
2018-11-23 10:15:46 +02:00
. newInstance ( script , [ : ] ) . loadStepDefaults ( )
2019-03-21 14:25:22 +02:00
}
2018-10-16 13:07:38 +02:00
2019-05-23 17:37:47 +02:00
static Map getYamlResource ( String resource ) {
def ymlContent = new File ( projectRoot , "resources/${resource}" ) . text
return new Yaml ( ) . load ( ymlContent )
}
2019-05-31 09:36:12 +02:00
static getDummyScript ( def stepName ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def _stepName = stepName
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
return new Script ( ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def STEP_NAME = _stepName
2018-10-16 13:07:38 +02:00
2019-05-31 09:36:12 +02:00
def handlePipelineStepErrors ( def m , Closure c ) {
c ( )
}
def libraryResource ( def r ) {
new File ( projectRoot , "resources/${r}" ) . text
}
def readYaml ( def m ) {
new Yaml ( ) . load ( m . text )
}
2019-03-21 14:25:22 +02:00
2019-05-31 09:36:12 +02:00
void echo ( m ) {
println ( m )
2018-11-05 10:51:28 +02:00
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def run ( ) {
throw new UnsupportedOperationException ( )
}
}
2018-10-26 11:48:11 +02:00
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
static trim ( List lines ) {
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
removeLeadingEmptyLines (
removeLeadingEmptyLines ( lines . reverse ( ) )
. reverse ( ) )
}
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
private static removeLeadingEmptyLines ( lines ) {
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
def _lines = new ArrayList ( lines ) , trimmed = [ ]
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
boolean empty = true
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
_lines . each ( ) {
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
if ( empty & & ! it . trim ( ) ) return
empty = false
trimmed < < it
}
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
trimmed
}
2018-11-02 14:30:32 +02:00
2018-11-05 10:51:28 +02:00
private static normalize ( Set p ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def normalized = [ ] as Set
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def interim = [ : ]
p . each {
def parts = it . split ( '/' ) as List
_normalize ( parts , interim )
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
interim . each { k , v - > flatten ( normalized , k , v ) }
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
normalized
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
private static void _normalize ( List parts , Map interim ) {
if ( parts . size > = 1 ) {
if ( ! interim [ parts . head ( ) ] ) interim [ parts . head ( ) ] = [ : ]
_normalize ( parts . tail ( ) , interim [ parts . head ( ) ] )
}
2018-10-26 11:48:11 +02:00
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
private static flatten ( Set flat , def key , Map interim ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
if ( ! interim ) flat < < ( key as String )
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
interim . each { k , v - >
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def _key = "${key}/${k}"
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
if ( v & & v . size ( ) > 0 )
flatten ( flat , _key , v )
else
flat < < ( _key as String )
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
}
2018-10-16 13:07:38 +02:00
}
2018-11-05 10:51:28 +02:00
static void scanDocu ( File f , Map step ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
boolean docu = false ,
value = false ,
2018-11-23 13:38:15 +02:00
mandatory = false ,
2019-03-21 14:25:22 +02:00
parentObject = false ,
2018-11-05 10:51:28 +02:00
docuEnd = false
2018-10-16 13:07:38 +02:00
2019-03-21 14:25:22 +02:00
def docuLines = [ ] , valueLines = [ ] , mandatoryLines = [ ] , parentObjectLines = [ ]
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
f . eachLine {
line - >
2018-10-16 13:07:38 +02:00
2019-05-08 11:36:01 +02:00
if ( line = = ~ /.*dependingOn.*/ ) {
def dependentConfigKey = ( line = ~ /.*dependingOn\('(.*)'\).mixin\('(.*)'/ ) [ 0 ] [ 1 ]
def configKey = ( line = ~ /.*dependingOn\('(.*)'\).mixin\('(.*)'/ ) [ 0 ] [ 2 ]
if ( ! step . dependentConfig [ configKey ] ) {
step . dependentConfig [ configKey ] = [ ]
}
step . dependentConfig [ configKey ] < < dependentConfigKey
2019-04-30 16:39:29 +02:00
}
2019-05-08 11:36:01 +02:00
if ( docuEnd ) {
docuEnd = false
2018-11-05 10:51:28 +02:00
2019-05-08 11:36:01 +02:00
if ( isHeader ( line ) ) {
def _docu = [ ]
docuLines . each { _docu < < it }
_docu = Helper . trim ( _docu )
step . description = _docu . join ( '\n' )
} else {
2018-11-05 10:51:28 +02:00
2019-05-08 11:36:01 +02:00
def param = retrieveParameterName ( line )
2018-11-05 10:51:28 +02:00
2019-05-08 11:36:01 +02:00
if ( ! param ) {
throw new RuntimeException ( 'Cannot retrieve parameter for a comment' )
}
2018-11-05 10:51:28 +02:00
2019-05-08 11:36:01 +02:00
def _docu = [ ] , _value = [ ] , _mandatory = [ ] , _parentObject = [ ]
docuLines . each { _docu < < it }
valueLines . each { _value < < it }
mandatoryLines . each { _mandatory < < it }
parentObjectLines . each { _parentObject < < it }
_parentObject < < param
param = _parentObject * . trim ( ) . join ( '/' ) . trim ( )
2019-03-21 14:25:22 +02:00
2019-05-08 11:36:01 +02:00
if ( step . parameters [ param ] . docu | | step . parameters [ param ] . value )
System . err < < "[WARNING] There is already some documentation for parameter '${param}. Is this parameter documented twice?'\n"
2018-11-05 10:51:28 +02:00
2019-05-08 11:36:01 +02:00
step . parameters [ param ] . docu = _docu * . trim ( ) . join ( ' ' ) . trim ( )
step . parameters [ param ] . value = _value * . trim ( ) . join ( ' ' ) . trim ( )
step . parameters [ param ] . mandatory = _mandatory * . trim ( ) . join ( ' ' ) . trim ( )
}
docuLines . clear ( )
valueLines . clear ( )
mandatoryLines . clear ( )
parentObjectLines . clear ( )
2018-11-05 10:51:28 +02:00
}
2018-10-29 08:45:09 +02:00
2019-05-08 11:36:01 +02:00
if ( line . trim ( ) = = ~ /^\/ \ * \ * . * / ) {
docu = true
2018-11-23 13:38:15 +02:00
}
2018-11-05 10:51:28 +02:00
2019-05-08 11:36:01 +02:00
if ( docu ) {
def _line = line
_line = _line . replaceAll ( '^\\s*' , '' ) // leading white spaces
if ( _line . startsWith ( '/**' ) ) _line = _line . replaceAll ( '^\\/\\*\\*' , '' ) // start comment
if ( _line . startsWith ( '*/' ) | | _line . trim ( ) . endsWith ( '*/' ) ) _line = _line . replaceAll ( '^\\*/' , '' ) . replaceAll ( '\\*/\\s*$' , '' ) // end comment
if ( _line . startsWith ( '*' ) ) _line = _line . replaceAll ( '^\\*' , '' ) // continue comment
if ( _line . startsWith ( ' ' ) ) _line = _line . replaceAll ( '^\\s' , '' )
if ( _line = = ~ /.*@possibleValues.*/ ) {
mandatory = false // should be something like reset attributes
value = true
parentObject = false
}
// some remark for mandatory e.g. some parameters are only mandatory under certain conditions
if ( _line = = ~ /.*@mandatory.*/ ) {
value = false // should be something like reset attributes ...
mandatory = true
parentObject = false
}
// grouping config properties within a parent object for easier readability
if ( _line = = ~ /.*@parentConfigKey.*/ ) {
value = false // should be something like reset attributes ...
mandatory = false
parentObject = true
2018-11-05 10:51:28 +02:00
}
2018-11-23 13:38:15 +02:00
2019-05-08 11:36:01 +02:00
if ( value ) {
if ( _line ) {
_line = ( _line = ~ /.*@possibleValues\s*?(.*)/ ) [ 0 ] [ 1 ]
valueLines < < _line
}
2018-11-23 13:38:15 +02:00
}
2019-05-08 11:36:01 +02:00
if ( mandatory ) {
if ( _line ) {
_line = ( _line = ~ /.*@mandatory\s*?(.*)/ ) [ 0 ] [ 1 ]
mandatoryLines < < _line
}
}
if ( parentObject ) {
if ( _line ) {
_line = ( _line = ~ /.*@parentConfigKey\s*?(.*)/ ) [ 0 ] [ 1 ]
parentObjectLines < < _line
}
2019-03-21 14:25:22 +02:00
}
2019-05-08 11:36:01 +02:00
if ( ! value & & ! mandatory & & ! parentObject ) {
docuLines < < _line
}
2018-11-05 10:51:28 +02:00
}
2018-10-26 12:21:06 +02:00
2019-05-08 11:36:01 +02:00
if ( docu & & line . trim ( ) = = ~ /^.*\*\/ / ) {
docu = false
value = false
mandatory = false
parentObject = false
docuEnd = true
}
2018-11-05 10:51:28 +02:00
}
}
2018-10-29 08:48:21 +02:00
2018-11-05 10:51:28 +02:00
private static isHeader ( line ) {
2018-11-20 17:51:55 +02:00
Matcher headerMatcher = ( line = ~ /(?:(?:def|void)\s*call\s*\()|(?:@.*)/ )
return headerMatcher . size ( ) = = 1
2018-11-05 10:51:28 +02:00
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
private static retrieveParameterName ( line ) {
Matcher m = ( line = ~ /.*'(.*)'.*/ )
if ( m . size ( ) = = 1 & & m [ 0 ] . size ( ) = = 2 )
return m [ 0 ] [ 1 ]
return null
}
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
static getScopedParameters ( def script ) {
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
def params = [ : ]
2018-10-16 13:07:38 +02:00
2018-11-05 10:51:28 +02:00
params . put ( 'STEP_CONFIG' , script . STEP_CONFIG_KEYS ? : [ ] )
params . put ( 'GENERAL_CONFIG' , script . GENERAL_CONFIG_KEYS ? : [ ] )
2019-03-15 16:24:06 +02:00
params . put ( 'STAGE_CONFIG' , script . PARAMETER_KEYS ? : [ ] )
2018-11-05 10:51:28 +02:00
return params
2018-10-16 13:07:38 +02:00
}
2018-11-05 10:51:28 +02:00
2019-05-23 17:37:47 +02:00
static getStageStepKeys ( def script ) {
try {
return script . STAGE_STEP_KEYS ? : [ ]
} catch ( groovy . lang . MissingPropertyException ex ) {
System . err < < "[INFO] STAGE_STEP_KEYS not set for: ${script.STEP_NAME}.\n"
return [ ]
}
}
2018-11-05 10:51:28 +02:00
static getRequiredParameters ( File f ) {
def params = [ ] as Set
f . eachLine {
line - >
2019-07-31 12:51:55 +02:00
if ( line = = ~ /.*withMandatoryProperty\(.*/ ) {
2019-03-21 14:25:22 +02:00
def param = ( line = ~ /.*withMandatoryProperty\('(.*)'/ ) [ 0 ] [ 1 ]
params < < param
}
2018-11-05 10:51:28 +02:00
}
return params
}
2019-03-21 14:25:22 +02:00
static getParentObjectMappings ( File f ) {
def mappings = [ : ]
def parentObjectKey = ''
f . eachLine {
line - >
if ( line = = ~ /.*parentConfigKey.*/ & & ! parentObjectKey ) {
def param = ( line = ~ /.*parentConfigKey\s*?(.*)/ ) [ 0 ] [ 1 ]
parentObjectKey = param . trim ( )
} else if ( line = = ~ /\s*?(.*)[,]{0,1}/ & & parentObjectKey ) {
def pName = retrieveParameterName ( line )
if ( pName ) {
mappings . put ( pName , parentObjectKey )
parentObjectKey = ''
}
}
}
return mappings
}
2019-01-11 16:18:51 +02:00
static resolveDocuRelevantSteps ( GroovyScriptEngine gse , File stepsDir ) {
def docuRelevantSteps = [ ]
stepsDir . traverse ( type: FileType . FILES , maxDepth: 0 ) {
if ( it . getName ( ) . endsWith ( '.groovy' ) ) {
2019-02-18 09:20:16 +02:00
def scriptName = ( it = ~ /vars\${File.separator}(.*)\.groovy/ ) [ 0 ] [ 1 ]
2019-01-11 16:18:51 +02:00
def stepScript = gse . createScript ( "${scriptName}.groovy" , new Binding ( ) )
for ( def method in stepScript . getClass ( ) . getMethods ( ) ) {
2019-05-23 17:37:47 +02:00
if ( method . getName ( ) = = 'call' & & ( method . getAnnotation ( GenerateDocumentation ) ! = null | | method . getAnnotation ( GenerateStageDocumentation ) ! = null ) ) {
2019-01-11 16:18:51 +02:00
docuRelevantSteps < < scriptName
break
}
}
}
}
docuRelevantSteps
}
2019-05-23 17:37:47 +02:00
static resolveDocuRelevantStages ( GroovyScriptEngine gse , File stepsDir ) {
def docuRelevantStages = [ : ]
stepsDir . traverse ( type: FileType . FILES , maxDepth: 0 ) {
if ( it . getName ( ) . endsWith ( '.groovy' ) ) {
def scriptName = ( it = ~ /vars\${File.separator}(.*)\.groovy/ ) [ 0 ] [ 1 ]
def stepScript = gse . createScript ( "${scriptName}.groovy" , new Binding ( ) )
for ( def method in stepScript . getClass ( ) . getMethods ( ) ) {
GenerateStageDocumentation stageDocsAnnotation = method . getAnnotation ( GenerateStageDocumentation )
if ( method . getName ( ) = = 'call' & & stageDocsAnnotation ! = null ) {
docuRelevantStages [ scriptName ] = stageDocsAnnotation . defaultStageName ( )
break
}
}
}
}
docuRelevantStages
}
2018-10-26 11:48:11 +02:00
}
roots = [
2019-03-08 13:33:31 +02:00
new File ( Helper . projectRoot , "vars" ) . getAbsolutePath ( ) ,
new File ( Helper . projectRoot , "src" ) . getAbsolutePath ( )
2019-05-08 11:36:01 +02:00
]
2018-10-26 11:48:11 +02:00
stepsDir = null
stepsDocuDir = null
2019-05-23 17:37:47 +02:00
stagesDocuDir = null
customDefaults = null
2018-10-26 11:48:11 +02:00
2018-10-26 12:08:34 +02:00
steps = [ ]
2018-10-26 11:48:11 +02:00
//
// assign parameters
2019-04-12 15:32:30 +02:00
def cli = new CliBuilder (
usage: 'groovy createDocu [<options>]' ,
header: 'Options:' ,
footer: 'Copyright: SAP SE' )
2018-10-26 11:48:11 +02:00
2019-04-12 15:32:30 +02:00
cli . with {
s longOpt: 'stepsDir' , args: 1 , argName: 'dir' , 'The directory containing the steps. Defaults to \'vars\'.'
d longOpt: 'docuDir' , args: 1 , argName: 'dir' , 'The directory containing the docu stubs. Defaults to \'documentation/docs/steps\'.'
2019-05-23 17:37:47 +02:00
p longOpt: 'docuDirStages' , args: 1 , argName: 'dir' , 'The directory containing the docu stubs for pipeline stages. Defaults to \'documentation/docs/stages\'.'
2019-05-17 09:58:43 +02:00
c longOpt: 'customDefaults' , args: 1 , argName: 'file' , 'Additional custom default configuration'
2019-05-23 17:37:47 +02:00
i longOpt: 'stageInitFile' , args: 1 , argName: 'file' , 'The file containing initialization data for step piperInitRunStageConfiguration'
2019-04-12 15:32:30 +02:00
h longOpt: 'help' , 'Prints this help.'
}
2018-10-26 11:48:11 +02:00
2019-04-12 15:32:30 +02:00
def options = cli . parse ( args )
if ( options . h ) {
System . err < < "Printing help.\n"
cli . usage ( )
return
}
2018-10-26 11:48:11 +02:00
2019-05-23 17:37:47 +02:00
if ( options . s ) {
System . err < < "[INFO] Using custom step root: ${options.s}.\n"
2019-04-12 15:32:30 +02:00
stepsDir = new File ( Helper . projectRoot , options . s )
2019-05-23 17:37:47 +02:00
}
2018-10-26 11:48:11 +02:00
2019-03-08 13:33:31 +02:00
stepsDir = stepsDir ? : new File ( Helper . projectRoot , "vars" )
2018-10-26 11:48:11 +02:00
2019-05-23 17:37:47 +02:00
if ( options . d ) {
System . err < < "[INFO] Using custom doc dir for steps: ${options.d}.\n"
2019-04-12 15:32:30 +02:00
stepsDocuDir = new File ( Helper . projectRoot , options . d )
2019-05-23 17:37:47 +02:00
}
2018-10-26 11:48:11 +02:00
2019-03-08 13:33:31 +02:00
stepsDocuDir = stepsDocuDir ? : new File ( Helper . projectRoot , "documentation/docs/steps" )
2018-10-26 11:48:11 +02:00
2019-05-23 17:37:47 +02:00
if ( options . p ) {
System . err < < "[INFO] Using custom doc dir for stages: ${options.p}.\n"
stagesDocuDir = new File ( Helper . projectRoot , options . p )
}
stagesDocuDir = stagesDocuDir ? : new File ( Helper . projectRoot , "documentation/docs/stages" )
2019-05-17 09:58:43 +02:00
if ( options . c ) {
2019-05-23 17:37:47 +02:00
System . err < < "[INFO] Using custom defaults: ${options.c}.\n"
2019-05-17 09:58:43 +02:00
customDefaults = options . c
2019-04-10 12:17:29 +02:00
}
2018-10-26 12:08:34 +02:00
2019-06-19 09:46:10 +02:00
// retrieve default conditions for steps
Map stageConfig
if ( options . i ) {
System . err < < "[INFO] Using stageInitFile ${options.i}.\n"
stageConfig = Helper . getYamlResource ( options . i )
System . err < < "[INFO] Default stage configuration: ${stageConfig}.\n"
}
2019-04-12 15:32:30 +02:00
steps . addAll ( options . arguments ( ) )
2018-10-26 12:08:34 +02:00
2018-10-26 11:48:11 +02:00
// assign parameters
//
//
// sanity checks
if ( ! stepsDocuDir . exists ( ) ) {
2018-11-05 11:04:04 +02:00
System . err < < "Steps docu dir '${stepsDocuDir}' does not exist.\n"
System . exit ( 1 )
2018-10-26 11:48:11 +02:00
}
if ( ! stepsDir . exists ( ) ) {
2018-11-05 11:04:04 +02:00
System . err < < "Steps dir '${stepsDir}' does not exist.\n"
System . exit ( 1 )
2018-10-26 11:48:11 +02:00
}
// sanity checks
//
2019-03-08 13:33:31 +02:00
def gse = new GroovyScriptEngine ( [ stepsDir . getAbsolutePath ( ) ] as String [ ] , GenerateDocumentation . class . getClassLoader ( ) )
2018-10-26 11:48:11 +02:00
//
2018-10-26 12:08:34 +02:00
// find all the steps we have to document (if no step has been provided from outside)
if ( ! steps ) {
2019-01-11 16:18:51 +02:00
steps = Helper . resolveDocuRelevantSteps ( gse , stepsDir )
2018-10-26 12:08:34 +02:00
} else {
2018-11-05 11:04:04 +02:00
System . err < < "[INFO] Generating docu only for step ${steps.size > 1 ? 's' : ''} ${steps}.\n"
2018-10-26 12:08:34 +02:00
}
2018-10-26 11:48:11 +02:00
2019-05-23 17:37:47 +02:00
// find all the stages that we have to document
Map stages = Helper . resolveDocuRelevantStages ( gse , stepsDir )
2018-10-26 11:48:11 +02:00
boolean exceptionCaught = false
2018-10-29 14:54:31 +02:00
def stepDescriptors = [ : ]
2019-05-31 09:36:12 +02:00
DefaultValueCache . prepare ( Helper . getDummyScript ( 'noop' ) , customDefaults )
2018-10-26 11:48:11 +02:00
for ( step in steps ) {
2018-11-05 11:04:04 +02:00
try {
2019-05-31 09:36:12 +02:00
stepDescriptors . "${step}" = handleStep ( step , gse )
2018-11-05 11:04:04 +02:00
} catch ( Exception e ) {
exceptionCaught = true
2019-07-30 13:41:58 +02:00
def writer = new StringWriter ( )
e . printStackTrace ( new PrintWriter ( writer ) )
System . err < < "${e.getClass().getName()} caught while handling step '${step}': ${e.getMessage()}.\n${writer.toString()}\n"
2018-11-05 11:04:04 +02:00
}
2018-10-26 11:48:11 +02:00
}
2018-10-29 14:54:31 +02:00
2019-02-01 16:54:11 +02:00
// replace @see tag in docu by docu from referenced step.
for ( step in stepDescriptors ) {
2019-07-03 10:13:26 +02:00
if ( step . value ? . parameters ) {
2019-02-01 16:54:11 +02:00
for ( param in step . value . parameters ) {
if ( param ? . value ? . docu ? . contains ( '@see' ) ) {
def otherStep = param . value . docu . replaceAll ( '@see' , '' ) . trim ( )
param . value . docu = fetchTextFrom ( otherStep , param . key , stepDescriptors )
param . value . mandatory = fetchMandatoryFrom ( otherStep , param . key , stepDescriptors )
2019-04-11 16:30:22 +02:00
if ( ! param . value . value )
param . value . value = fetchPossibleValuesFrom ( otherStep , param . key , stepDescriptors )
2019-02-01 16:54:11 +02:00
}
}
}
}
2019-05-23 17:37:47 +02:00
//update stepDescriptors: remove stages and put into separate stageDescriptors map
def stageDescriptors = [ : ]
stages . each { key , value - >
System . err < < "[INFO] Processing stage '${key}' ...\n"
2019-07-03 10:13:26 +02:00
if ( stepDescriptors . "${key}" ) {
stageDescriptors . "${key}" = [ : ] < < stepDescriptors . "${key}"
stepDescriptors . remove ( key )
} else {
stageDescriptors . "${key}" = [ : ]
}
2019-05-23 17:37:47 +02:00
//add stage name to stageDescriptors
stageDescriptors . "${key}" . name = value
//add stepCondition informmation to stageDescriptors
stageDescriptors . "${key}" . configConditions = stageConfig ? . stages ? . get ( value ) ? . stepConditions
//identify step keys in stages
def stageStepKeys = Helper . getStageStepKeys ( gse . createScript ( "${key}.groovy" , new Binding ( ) ) )
// prepare step descriptions
stageDescriptors . "${key}" . stepDescriptions = [ : ]
stageDescriptors . "${key}" . parameters . each { paramKey , paramValue - >
if ( paramKey in stageStepKeys ) {
stageDescriptors . "${key}" . stepDescriptions . "${paramKey}" = "${paramValue.docu ?: ''}\n"
}
}
//remove details from parameter map
stageStepKeys . each { stepKey - >
stageDescriptors . "${key}" . parameters . remove ( stepKey )
}
}
2018-10-29 14:54:31 +02:00
for ( step in stepDescriptors ) {
2018-11-05 11:04:04 +02:00
try {
renderStep ( step . key , step . value )
System . err < < "[INFO] Step '${step.key}' has been rendered.\n"
} catch ( Exception e ) {
exceptionCaught = true
System . err < < "${e.getClass().getName()} caught while rendering step '${step}': ${e.getMessage()}.\n"
2018-11-05 11:14:01 +02:00
}
2018-10-29 14:54:31 +02:00
}
2019-05-23 17:37:47 +02:00
for ( stage in stageDescriptors ) {
try {
renderStage ( stage . key , stage . value )
System . err < < "[INFO] Stage '${stage.key}' has been rendered.\n"
} catch ( Exception e ) {
exceptionCaught = true
System . err < < "${e.getClass().getName()} caught while rendering stage '${stage}': ${e.getMessage()}.\n"
}
}
2018-10-26 11:48:11 +02:00
if ( exceptionCaught ) {
2018-11-05 11:04:04 +02:00
System . err < < "[ERROR] Exception caught during generating documentation. Check earlier log for details.\n"
System . exit ( 1 )
2018-10-26 11:48:11 +02:00
}
2019-03-27 18:21:08 +02:00
File docuMetaData = new File ( 'target/docuMetaData.json' )
if ( docuMetaData . exists ( ) ) docuMetaData . delete ( )
docuMetaData < < new JsonOutput ( ) . toJson ( stepDescriptors )
2018-10-29 14:39:37 +02:00
System . err < < "[INFO] done.\n"
2018-10-26 11:48:11 +02:00
2018-10-29 14:54:31 +02:00
void renderStep ( stepName , stepProperties ) {
2018-11-05 11:04:04 +02:00
File theStepDocu = new File ( stepsDocuDir , "${stepName}.md" )
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
if ( ! theStepDocu . exists ( ) ) {
System . err < < "[WARNING] step docu input file for step '${stepName}' is missing.\n"
return
}
2018-10-26 11:48:11 +02:00
2019-02-18 09:20:16 +02:00
def binding = [
docGenStepName : stepName ,
docGenDescription : 'Description\n\n' + stepProperties . description ,
docGenParameters : 'Parameters\n\n' + TemplateHelper . createParametersSection ( stepProperties . parameters ) ,
2019-05-20 09:32:49 +02:00
docGenConfiguration : 'Step configuration\n\n' + TemplateHelper . createStepConfigurationSection ( stepProperties . parameters ) ,
2019-05-24 15:50:30 +02:00
docJenkinsPluginDependencies : 'Dependencies\n\n' + TemplateHelper . createDependencyList ( stepProperties . dependencies )
2019-02-18 09:20:16 +02:00
]
2019-05-20 11:16:59 +02:00
2019-02-18 09:20:16 +02:00
def template = new StreamingTemplateEngine ( ) . createTemplate ( theStepDocu . text )
String text = template . make ( binding )
2018-11-02 15:52:43 +02:00
2018-11-05 11:04:04 +02:00
theStepDocu . withWriter { w - > w . write text }
2018-10-29 14:54:31 +02:00
}
2019-05-23 17:37:47 +02:00
void renderStage ( stageName , stageProperties ) {
def stageFileName = stageName . indexOf ( 'Stage' ) ! = - 1 ? stageName . split ( 'Stage' ) [ 1 ] . toLowerCase ( ) : stageFileName
File theStageDocu = new File ( stagesDocuDir , "${stageFileName}.md" )
if ( ! theStageDocu . exists ( ) ) {
System . err < < "[WARNING] stage docu input file for stage '${stageName}' is missing.\n"
return
}
def binding = [
docGenStageName : stageProperties . name ,
docGenDescription : stageProperties . description ,
docGenStageContent : 'Stage Content\n\n' + TemplateHelper . createStageContentSection ( stageProperties . stepDescriptions ) ,
docGenStageActivation: 'Stage Activation\n\n' + TemplateHelper . createStageActivationSection ( ) ,
docGenStepActivation: 'Step Activation\n\n' + TemplateHelper . createStepActivationSection ( stageProperties . configConditions ) ,
docGenStageParameters : 'Additional Stage Parameters\n\n' + TemplateHelper . createParametersSection ( stageProperties . parameters ) ,
docGenStageConfiguration : 'Configuration of Additional Stage Parameters\n\n' + TemplateHelper . createStageConfigurationSection ( )
]
def template = new StreamingTemplateEngine ( ) . createTemplate ( theStageDocu . text )
String text = template . make ( binding )
theStageDocu . withWriter { w - > w . write text }
}
2019-02-01 16:54:11 +02:00
def fetchTextFrom ( def step , def parameterName , def steps ) {
try {
def docuFromOtherStep = steps [ step ] ? . parameters [ parameterName ] ? . docu
if ( ! docuFromOtherStep ) throw new IllegalStateException ( "No docu found for parameter '${parameterName}' in step ${step}." )
return docuFromOtherStep
} catch ( e ) {
System . err < < "[ERROR] Cannot retrieve docu for parameter ${parameterName} from step ${step}.\n"
throw e
}
}
def fetchMandatoryFrom ( def step , def parameterName , def steps ) {
try {
return steps [ step ] ? . parameters [ parameterName ] ? . mandatory
} catch ( e ) {
System . err < < "[ERROR] Cannot retrieve docu for parameter ${parameterName} from step ${step}.\n"
throw e
}
}
2019-04-11 16:30:22 +02:00
def fetchPossibleValuesFrom ( def step , def parameterName , def steps ) {
2019-05-08 11:36:01 +02:00
return steps [ step ] ? . parameters [ parameterName ] ? . value ? : ''
2019-04-11 16:30:22 +02:00
}
2019-05-31 09:36:12 +02:00
def handleStep ( stepName , gse ) {
2018-10-29 14:54:31 +02:00
2018-11-05 11:04:04 +02:00
File theStep = new File ( stepsDir , "${stepName}.groovy" )
File theStepDocu = new File ( stepsDocuDir , "${stepName}.md" )
2019-05-21 14:01:43 +02:00
File theStepDeps = new File ( 'documentation/jenkins_workspace/plugin_mapping.json' )
2018-10-29 14:54:31 +02:00
2019-05-23 17:37:47 +02:00
if ( ! theStepDocu . exists ( ) & & stepName . indexOf ( 'Stage' ) ! = - 1 ) {
//try to get a corresponding stage documentation
def stageName = stepName . split ( 'Stage' ) [ 1 ] . toLowerCase ( )
theStepDocu = new File ( stagesDocuDir , "${stageName}.md" )
}
2018-11-05 11:04:04 +02:00
if ( ! theStepDocu . exists ( ) ) {
System . err < < "[WARNING] step docu input file for step '${stepName}' is missing.\n"
return
}
2018-10-29 14:54:31 +02:00
2018-11-05 11:04:04 +02:00
System . err < < "[INFO] Handling step '${stepName}'.\n"
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
def defaultConfig = Helper . getConfigHelper ( getClass ( ) . getClassLoader ( ) ,
2019-05-08 11:36:01 +02:00
roots ,
2019-05-31 09:36:12 +02:00
Helper . getDummyScript ( stepName ) ) . use ( )
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
def params = [ ] as Set
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
//
// scopedParameters is a map containing the scope as key and the parameters
// defined with that scope as a set of strings.
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
def scopedParameters
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
try {
scopedParameters = Helper . getScopedParameters ( gse . createScript ( "${stepName}.groovy" , new Binding ( ) ) )
scopedParameters . each { k , v - > params . addAll ( v ) }
} catch ( Exception e ) {
System . err < < "[ERROR] Step '${stepName}' violates naming convention for scoped parameters: ${e}.\n"
throw e
}
def requiredParameters = Helper . getRequiredParameters ( theStep )
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
params . addAll ( requiredParameters )
2018-10-26 11:48:11 +02:00
2019-03-21 14:25:22 +02:00
// translate parameter names according to compatibility annotations
def parentObjectMappings = Helper . getParentObjectMappings ( theStep )
def compatibleParams = [ ] as Set
if ( parentObjectMappings ) {
params . each {
2019-05-08 11:36:01 +02:00
if ( parentObjectMappings [ it ] )
compatibleParams . add ( parentObjectMappings [ it ] + '/' + it )
else
compatibleParams . add ( it )
2019-03-21 14:25:22 +02:00
}
if ( compatibleParams )
params = compatibleParams
}
2019-04-30 16:39:29 +02:00
// 'dependentConfig' is only present here for internal reasons and that entry is removed at
// end of method.
2019-05-20 11:16:59 +02:00
def step = [
2019-05-24 16:40:31 +02:00
parameters: [ : ] ,
dependencies: ( Set ) [ ] ,
dependentConfig: [ : ]
]
2019-05-20 11:16:59 +02:00
2019-05-20 11:45:28 +02:00
//
// provide dependencies to Jenkins plugins
2019-05-20 11:16:59 +02:00
if ( theStepDeps . exists ( ) ) {
2019-05-24 16:40:31 +02:00
def pluginDependencies = new JsonSlurper ( ) . parse ( theStepDeps )
step . dependencies . addAll ( pluginDependencies [ stepName ] . collect { k , v - > k } )
2019-05-20 11:16:59 +02:00
}
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
//
// START special handling for 'script' parameter
// ... would be better if there is no special handling required ...
2018-10-29 14:09:20 +02:00
2018-11-05 11:04:04 +02:00
step . parameters [ 'script' ] = [
2019-05-08 11:36:01 +02:00
docu: 'The common script environment of the Jenkinsfile running. ' +
'Typically the reference to the script calling the pipeline ' +
2019-05-22 08:16:07 +02:00
'step is provided with the `this` parameter, as in `script: this`. ' +
2019-05-08 11:36:01 +02:00
'This allows the function to access the ' +
2019-05-22 08:16:07 +02:00
'`commonPipelineEnvironment` for retrieving, e.g. configuration parameters.' ,
2019-05-08 11:36:01 +02:00
required: true ,
GENERAL_CONFIG: false ,
STEP_CONFIG: false
]
2018-10-29 14:09:20 +02:00
2018-11-05 11:04:04 +02:00
// END special handling for 'script' parameter
2018-10-29 14:09:20 +02:00
2018-11-05 11:04:04 +02:00
Helper . normalize ( params ) . toSorted ( ) . each {
2018-10-26 11:48:11 +02:00
2018-11-05 11:04:04 +02:00
it - >
2018-10-26 11:48:11 +02:00
2019-05-15 20:52:45 +02:00
def defaultValue = MapUtils . getByPath ( defaultConfig , it )
2018-12-17 13:11:15 +02:00
2018-11-05 11:19:57 +02:00
def parameterProperties = [
2019-05-08 11:36:01 +02:00
defaultValue: defaultValue ,
required: requiredParameters . contains ( ( it as String ) ) & & defaultValue = = null
]
2018-10-26 11:48:11 +02:00
2019-03-21 14:25:22 +02:00
step . parameters . put ( it , parameterProperties )
2018-10-26 11:48:11 +02:00
2019-03-21 14:25:22 +02:00
// The scope is only defined for the first level of a hierarchical configuration.
// If the first part is found, all nested parameters are allowed with that scope.
def firstPart = it . split ( '/' ) . head ( )
scopedParameters . each { key , val - >
parameterProperties . put ( key , val . contains ( firstPart ) )
}
2018-10-26 11:48:11 +02:00
}
2018-11-05 11:04:04 +02:00
Helper . scanDocu ( theStep , step )
2018-10-26 11:48:11 +02:00
2019-04-30 16:39:29 +02:00
step . parameters . each { k , v - >
if ( step . dependentConfig . get ( k ) ) {
def dependentParameterKey = step . dependentConfig . get ( k ) [ 0 ]
def dependentValues = step . parameters . get ( dependentParameterKey ) ? . value
if ( dependentValues ) {
def the_defaults = [ ]
dependentValues
2019-05-03 09:49:41 +02:00
. replaceAll ( '[\'"` ]' , '' )
2019-04-30 16:39:29 +02:00
. split ( ',' ) . each { possibleValue - >
if ( ! possibleValue instanceof Boolean & & defaultConfig . get ( possibleValue ) ) {
the_defaults < <
[
dependentParameterKey: dependentParameterKey ,
key: possibleValue ,
2019-05-15 20:52:45 +02:00
value: MapUtils . getByPath ( defaultConfig . get ( possibleValue ) , k )
2019-04-30 16:39:29 +02:00
]
}
}
v . defaultValue = the_defaults
}
}
}
//
// 'dependentConfig' is only present for internal purposes and must not be used outside.
step . remove ( 'dependentConfig' )
2018-11-05 11:04:04 +02:00
step
2018-10-26 11:48:11 +02:00
}