2020-09-23 13:55:17 +02:00
package generator
import (
"bytes"
"fmt"
"io"
2021-11-18 08:24:00 +02:00
"net/url"
2020-09-23 13:55:17 +02:00
"os"
2021-11-18 08:24:00 +02:00
"path/filepath"
"strings"
2020-09-28 09:10:52 +02:00
"text/template"
2020-09-23 13:55:17 +02:00
"github.com/SAP/jenkins-library/pkg/config"
2021-11-18 08:24:00 +02:00
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/ghodss/yaml"
2020-09-23 13:55:17 +02:00
)
// DocuHelperData is used to transport the needed parameters and functions from the step generator to the docu generation.
type DocuHelperData struct {
DocTemplatePath string
OpenDocTemplateFile func ( d string ) ( io . ReadCloser , error )
DocFileWriter func ( f string , d [ ] byte , p os . FileMode ) error
OpenFile func ( s string ) ( io . ReadCloser , error )
}
var stepParameterNames [ ] string
2021-10-11 15:22:24 +02:00
var includeAzure bool
2020-09-23 13:55:17 +02:00
2020-10-16 12:50:39 +02:00
func readStepConfiguration ( stepMetadata config . StepData , customDefaultFiles [ ] string , docuHelperData DocuHelperData ) config . StepConfig {
filters := stepMetadata . GetParameterFilters ( )
filters . All = append ( filters . All , "collectTelemetryData" )
filters . General = append ( filters . General , "collectTelemetryData" )
filters . Parameters = append ( filters . Parameters , "collectTelemetryData" )
defaultFiles := [ ] io . ReadCloser { }
for _ , projectDefaultFile := range customDefaultFiles {
fc , _ := docuHelperData . OpenFile ( projectDefaultFile )
defer fc . Close ( )
defaultFiles = append ( defaultFiles , fc )
}
configuration := config . Config { }
stepConfiguration , err := configuration . GetStepConfig (
map [ string ] interface { } { } ,
"" ,
nil ,
defaultFiles ,
false ,
filters ,
2021-12-15 16:07:47 +02:00
stepMetadata ,
2020-10-16 12:50:39 +02:00
map [ string ] interface { } { } ,
"" ,
stepMetadata . Metadata . Name ,
)
checkError ( err )
return stepConfiguration
}
2020-09-23 13:55:17 +02:00
// GenerateStepDocumentation generates step coding based on step configuration provided in yaml files
2021-10-11 15:22:24 +02:00
func GenerateStepDocumentation ( metadataFiles [ ] string , customDefaultFiles [ ] string , docuHelperData DocuHelperData , azure bool ) error {
includeAzure = azure
2020-09-23 13:55:17 +02:00
for key := range metadataFiles {
2020-10-16 12:50:39 +02:00
stepMetadata := readStepMetadata ( metadataFiles [ key ] , docuHelperData )
2020-10-16 09:06:39 +02:00
2020-10-16 12:50:39 +02:00
adjustDefaultValues ( & stepMetadata )
2020-10-16 09:06:39 +02:00
2020-10-16 12:50:39 +02:00
stepConfiguration := readStepConfiguration ( stepMetadata , customDefaultFiles , docuHelperData )
applyCustomDefaultValues ( & stepMetadata , stepConfiguration )
adjustMandatoryFlags ( & stepMetadata )
2020-10-16 09:06:39 +02:00
2020-09-23 13:55:17 +02:00
fmt . Print ( " Generate documentation.. " )
2020-10-16 12:50:39 +02:00
if err := generateStepDocumentation ( stepMetadata , docuHelperData ) ; err != nil {
2020-09-23 13:55:17 +02:00
fmt . Println ( "" )
fmt . Println ( err )
} else {
fmt . Println ( "completed" )
}
}
return nil
}
// generates the step documentation and replaces the template with the generated documentation
func generateStepDocumentation ( stepData config . StepData , docuHelperData DocuHelperData ) error {
//create the file path for the template and open it.
docTemplateFilePath := fmt . Sprintf ( "%v%v.md" , docuHelperData . DocTemplatePath , stepData . Metadata . Name )
docTemplate , err := docuHelperData . OpenDocTemplateFile ( docTemplateFilePath )
if docTemplate != nil {
defer docTemplate . Close ( )
}
// check if there is an error during opening the template (true : skip docu generation for this meta data file)
if err != nil {
2020-09-24 07:41:06 +02:00
return fmt . Errorf ( "error occurred: %v" , err )
2020-09-23 13:55:17 +02:00
}
content := readAndAdjustTemplate ( docTemplate )
if len ( content ) <= 0 {
2020-09-24 07:41:06 +02:00
return fmt . Errorf ( "error occurred: no content inside of the template" )
2020-09-23 13:55:17 +02:00
}
// binding of functions and placeholder
tmpl , err := template . New ( "doc" ) . Funcs ( template . FuncMap {
"StepName" : createStepName ,
"Description" : createDescriptionSection ,
"Parameters" : createParametersSection ,
} ) . Parse ( content )
checkError ( err )
// add secrets, context defaults to the step parameters
handleStepParameters ( & stepData )
// write executed template data to the previously opened file
var docContent bytes . Buffer
err = tmpl . Execute ( & docContent , & stepData )
checkError ( err )
// overwrite existing file
err = docuHelperData . DocFileWriter ( docTemplateFilePath , docContent . Bytes ( ) , 644 )
checkError ( err )
return nil
}
func readContextInformation ( contextDetailsPath string , contextDetails * config . StepData ) {
contextDetailsFile , err := os . Open ( contextDetailsPath )
checkError ( err )
defer contextDetailsFile . Close ( )
err = contextDetails . ReadPipelineStepData ( contextDetailsFile )
checkError ( err )
}
func getContainerParameters ( container config . Container , sidecar bool ) map [ string ] interface { } {
containerParams := map [ string ] interface { } { }
if len ( container . Command ) > 0 {
containerParams [ ifThenElse ( sidecar , "sidecarCommand" , "containerCommand" ) ] = container . Command [ 0 ]
}
if len ( container . EnvVars ) > 0 {
containerParams [ ifThenElse ( sidecar , "sidecarEnvVars" , "dockerEnvVars" ) ] = config . EnvVarsAsMap ( container . EnvVars )
}
containerParams [ ifThenElse ( sidecar , "sidecarImage" , "dockerImage" ) ] = container . Image
containerParams [ ifThenElse ( sidecar , "sidecarPullImage" , "dockerPullImage" ) ] = container . ImagePullPolicy != "Never"
if len ( container . Name ) > 0 {
containerParams [ ifThenElse ( sidecar , "sidecarName" , "containerName" ) ] = container . Name
containerParams [ "dockerName" ] = container . Name
}
if len ( container . Options ) > 0 {
containerParams [ ifThenElse ( sidecar , "sidecarOptions" , "dockerOptions" ) ] = container . Options
}
if len ( container . WorkingDir ) > 0 {
containerParams [ ifThenElse ( sidecar , "sidecarWorkspace" , "dockerWorkspace" ) ] = container . WorkingDir
}
if sidecar {
if len ( container . ReadyCommand ) > 0 {
containerParams [ "sidecarReadyCommand" ] = container . ReadyCommand
}
} else {
if len ( container . Shell ) > 0 {
containerParams [ "containerShell" ] = container . Shell
}
}
//ToDo? add dockerVolumeBind, sidecarVolumeBind -> so far not part of config.Container
return containerParams
}
func handleStepParameters ( stepData * config . StepData ) {
stepParameterNames = stepData . GetParameterFilters ( ) . All
//add general options like script, verbose, etc.
//ToDo: add to context.yaml
appendGeneralOptionsToParameters ( stepData )
//consolidate conditional parameters:
//- remove duplicate parameter entries
//- combine defaults (consider conditions)
consolidateConditionalParameters ( stepData )
//get the context defaults
appendContextParameters ( stepData )
//consolidate context defaults:
//- combine defaults (consider conditions)
consolidateContextDefaults ( stepData )
setDefaultAndPossisbleValues ( stepData )
}
func setDefaultAndPossisbleValues ( stepData * config . StepData ) {
for k , param := range stepData . Spec . Inputs . Parameters {
//fill default if not set
if param . Default == nil {
switch param . Type {
case "bool" :
param . Default = false
case "int" :
param . Default = 0
}
}
//add possible values where known for certain types
switch param . Type {
case "bool" :
if param . PossibleValues == nil {
param . PossibleValues = [ ] interface { } { true , false }
}
}
stepData . Spec . Inputs . Parameters [ k ] = param
}
}
func appendGeneralOptionsToParameters ( stepData * config . StepData ) {
script := config . StepParameters {
Name : "script" , Type : "Jenkins Script" , Mandatory : true ,
Description : "The common script environment of the Jenkinsfile running. Typically the reference to the script calling the pipeline step is provided with the `this` parameter, as in `script: this`. This allows the function to access the `commonPipelineEnvironment` for retrieving, e.g. configuration parameters." ,
}
verbose := config . StepParameters {
Name : "verbose" , Type : "bool" , Mandatory : false , Default : false , Scope : [ ] string { "PARAMETERS" , "GENERAL" , "STEPS" , "STAGES" } ,
Description : "verbose output" ,
}
stepData . Spec . Inputs . Parameters = append ( stepData . Spec . Inputs . Parameters , script , verbose )
}
2021-11-18 08:24:00 +02:00
// GenerateStepDocumentation generates pipeline stage documentation based on pipeline configuration provided in a yaml file
func GenerateStageDocumentation ( stageMetadataPath , stageTargetPath , relativeStepsPath string , utils piperutils . FileUtils ) error {
if len ( stageTargetPath ) == 0 {
return fmt . Errorf ( "stageTargetPath cannot be empty" )
}
if len ( stageMetadataPath ) == 0 {
return fmt . Errorf ( "stageMetadataPath cannot be empty" )
}
if err := utils . MkdirAll ( stageTargetPath , 0777 ) ; err != nil {
return fmt . Errorf ( "failed to create directory '%v': %w" , stageTargetPath , err )
}
stageMetadataContent , err := utils . FileRead ( stageMetadataPath )
if err != nil {
return fmt . Errorf ( "failed to read stage metadata file '%v': %w" , stageMetadataPath , err )
}
stageRunConfig := config . RunConfigV1 { }
err = yaml . Unmarshal ( stageMetadataContent , & stageRunConfig . PipelineConfig )
if err != nil {
return fmt . Errorf ( "format of configuration is invalid %q: %w" , stageMetadataContent , err )
}
err = createPipelineDocumentation ( & stageRunConfig , stageTargetPath , relativeStepsPath , utils )
if err != nil {
return fmt . Errorf ( "failed to create pipeline documentation: %w" , err )
}
return nil
}
func createPipelineDocumentation ( stageRunConfig * config . RunConfigV1 , stageTargetPath , relativeStepsPath string , utils piperutils . FileUtils ) error {
if err := createPipelineOverviewDocumentation ( stageRunConfig , stageTargetPath , utils ) ; err != nil {
return fmt . Errorf ( "failed to create pipeline overview: %w" , err )
}
if err := createPipelineStageDocumentation ( stageRunConfig , stageTargetPath , relativeStepsPath , utils ) ; err != nil {
return fmt . Errorf ( "failed to create pipeline stage details: %w" , err )
}
return nil
}
func createPipelineOverviewDocumentation ( stageRunConfig * config . RunConfigV1 , stageTargetPath string , utils piperutils . FileUtils ) error {
overviewFileName := "overview.md"
overviewDoc := fmt . Sprintf ( "# %v\n\n" , stageRunConfig . PipelineConfig . Metadata . DisplayName )
overviewDoc += fmt . Sprintf ( "%v\n\n" , stageRunConfig . PipelineConfig . Metadata . Description )
2021-12-17 09:45:21 +02:00
overviewDoc += fmt . Sprintf ( "The %v comprises following stages\n\n" , stageRunConfig . PipelineConfig . Metadata . DisplayName )
2021-11-18 08:24:00 +02:00
for _ , stage := range stageRunConfig . PipelineConfig . Spec . Stages {
stageFilePath := filepath . Join ( stageTargetPath , fmt . Sprintf ( "%v.md" , stage . Name ) )
2021-12-17 09:45:21 +02:00
overviewDoc += fmt . Sprintf ( "* [%v Stage](%v)\n" , stage . DisplayName , stageFilePath )
2021-11-18 08:24:00 +02:00
}
overviewFilePath := filepath . Join ( stageTargetPath , overviewFileName )
fmt . Println ( "writing file" , overviewFilePath )
return utils . FileWrite ( overviewFilePath , [ ] byte ( overviewDoc ) , 0666 )
}
const stepConditionDetails = ` ! ! ! note "Step condition details"
There are currently several conditions which can be checked . < br / > * * Important : It will be sufficient that any one condition per step is met . * *
2021-12-17 09:45:21 +02:00
* ` + " ` " + `config` + " ` " + ` : Checks if a configuration parameter has a defined value .
* ` + " ` " + `config key` + " ` " + ` : Checks if a defined configuration parameter is set .
2021-11-18 08:24:00 +02:00
* ` + " ` " + `file pattern` + " ` " + ` : Checks if files according a defined pattern exist in the project .
* ` + " ` " + `file pattern from config` + " ` " + ` : Checks if files according a pattern defined in the custom configuration exist in the project .
* ` + " ` " + `npm script` + " ` " + ` : Checks if a npm script exists in one of the package . json files in the repositories .
`
const overrulingStepActivation = ` ! ! ! note "Overruling step activation conditions"
It is possible to overrule the automatically detected step activation status .
* In case a step will be * * active * * you can add to your stage configuration ` + " ` " + `<stepName>: false` + " ` " + ` to explicitly * * deactivate * * the step .
* In case a step will be * * inactive * * you can add to your stage configuration ` + " ` " + `<stepName>: true` + " ` " + ` to explicitly * * activate * * the step .
`
func createPipelineStageDocumentation ( stageRunConfig * config . RunConfigV1 , stageTargetPath , relativeStepsPath string , utils piperutils . FileUtils ) error {
for _ , stage := range stageRunConfig . PipelineConfig . Spec . Stages {
stageDoc := fmt . Sprintf ( "# %v\n\n" , stage . DisplayName )
stageDoc += fmt . Sprintf ( "%v\n\n" , stage . Description )
2021-12-13 12:35:41 +02:00
if len ( stage . Steps ) > 0 {
stageDoc += "## Stage Content\n\nThis stage comprises following steps which are activated depending on your use-case/configuration:\n\n"
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
for i , step := range stage . Steps {
if i == 0 {
stageDoc += "| step | step description |\n"
stageDoc += "| ---- | ---------------- |\n"
}
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
orchestratorBadges := ""
for _ , orchestrator := range step . Orchestrators {
orchestratorBadges += getBadge ( orchestrator ) + " "
}
stageDoc += fmt . Sprintf ( "| [%v](%v/%v.md) | %v%v |\n" , step . Name , relativeStepsPath , step . Name , orchestratorBadges , step . Description )
}
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
stageDoc += "\n"
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
stageDoc += "## Stage & Step Activation\n\nThis stage will be active in case one of following conditions are met:\n\n"
stageDoc += "* One of the steps is explicitly activated by using `<stepName>: true` in the stage configuration\n"
stageDoc += "* At least one of the step conditions is met and steps are not explicitly deactivated by using `<stepName>: false` in the stage configuration\n\n"
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
stageDoc += stepConditionDetails
stageDoc += overrulingStepActivation
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
stageDoc += "Following conditions apply for activation of steps contained in the stage:\n\n"
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
stageDoc += "| step | active if one of following conditions is met |\n"
stageDoc += "| ---- | -------------------------------------------- |\n"
2021-11-18 08:24:00 +02:00
2021-12-13 12:35:41 +02:00
// add step condition details
for _ , step := range stage . Steps {
stageDoc += fmt . Sprintf ( "| [%v](%v/%v.md) | %v |\n" , step . Name , relativeStepsPath , step . Name , getStepConditionDetails ( step ) )
}
2021-12-13 14:05:07 +02:00
}
2021-11-18 08:24:00 +02:00
2021-12-13 14:05:07 +02:00
stageFilePath := filepath . Join ( stageTargetPath , fmt . Sprintf ( "%v.md" , stage . Name ) )
fmt . Println ( "writing file" , stageFilePath )
if err := utils . FileWrite ( stageFilePath , [ ] byte ( stageDoc ) , 0666 ) ; err != nil {
return fmt . Errorf ( "failed to write stage file '%v': %w" , stageFilePath , err )
2021-11-18 08:24:00 +02:00
}
}
return nil
}
func getBadge ( orchestrator string ) string {
orchestratorOnly := strings . Title ( strings . ToLower ( orchestrator ) ) + " only"
urlPath := & url . URL { Path : orchestratorOnly }
orchestratorOnlyString := urlPath . String ( )
return fmt . Sprintf ( "[![%v](https://img.shields.io/badge/-%v-yellowgreen)](#)" , orchestratorOnly , orchestratorOnlyString )
}
func getStepConditionDetails ( step config . Step ) string {
stepConditions := ""
if step . Conditions == nil || len ( step . Conditions ) == 0 {
return "**active** by default - deactivate explicitly"
}
if len ( step . Orchestrators ) > 0 {
orchestratorBadges := ""
for _ , orchestrator := range step . Orchestrators {
orchestratorBadges += getBadge ( orchestrator ) + " "
}
stepConditions = orchestratorBadges + "<br />"
}
for _ , condition := range step . Conditions {
if condition . Config != nil && len ( condition . Config ) > 0 {
stepConditions += "<i>config:</i><ul>"
for param , activationValues := range condition . Config {
for _ , activationValue := range activationValues {
stepConditions += fmt . Sprintf ( "<li>`%v`: `%v`</li>" , param , activationValue )
}
// config condition only covers first entry
break
}
stepConditions += "</ul>"
continue
}
if len ( condition . ConfigKey ) > 0 {
2021-12-13 12:35:41 +02:00
stepConditions += fmt . Sprintf ( "<i>config key:</i> `%v`<br />" , condition . ConfigKey )
2021-11-18 08:24:00 +02:00
continue
}
if len ( condition . FilePattern ) > 0 {
2021-12-13 12:35:41 +02:00
stepConditions += fmt . Sprintf ( "<i>file pattern:</i> `%v`<br />" , condition . FilePattern )
2021-11-18 08:24:00 +02:00
continue
}
if len ( condition . FilePatternFromConfig ) > 0 {
2021-12-13 12:35:41 +02:00
stepConditions += fmt . Sprintf ( "<i>file pattern from config:</i> `%v`<br />" , condition . FilePatternFromConfig )
2021-11-18 08:24:00 +02:00
continue
}
if len ( condition . NpmScript ) > 0 {
2021-12-13 12:35:41 +02:00
stepConditions += fmt . Sprintf ( "<i>npm script:</i> `%v`<br />" , condition . NpmScript )
2021-11-18 08:24:00 +02:00
continue
}
if condition . Inactive {
stepConditions += "**inactive** by default - activate explicitly"
continue
}
}
return stepConditions
}