github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/documentation/generator/main.go (about) 1 package generator 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "net/url" 8 "os" 9 "path/filepath" 10 "strings" 11 "text/template" 12 13 "github.com/SAP/jenkins-library/pkg/config" 14 "github.com/SAP/jenkins-library/pkg/piperutils" 15 16 "github.com/ghodss/yaml" 17 ) 18 19 // DocuHelperData is used to transport the needed parameters and functions from the step generator to the docu generation. 20 type DocuHelperData struct { 21 DocTemplatePath string 22 OpenDocTemplateFile func(d string) (io.ReadCloser, error) 23 DocFileWriter func(f string, d []byte, p os.FileMode) error 24 OpenFile func(s string) (io.ReadCloser, error) 25 } 26 27 var stepParameterNames []string 28 var includeAzure bool 29 30 func readStepConfiguration(stepMetadata config.StepData, customDefaultFiles []string, docuHelperData DocuHelperData) config.StepConfig { 31 filters := stepMetadata.GetParameterFilters() 32 filters.All = append(filters.All, "collectTelemetryData") 33 filters.General = append(filters.General, "collectTelemetryData") 34 filters.Parameters = append(filters.Parameters, "collectTelemetryData") 35 36 defaultFiles := []io.ReadCloser{} 37 for _, projectDefaultFile := range customDefaultFiles { 38 fc, _ := docuHelperData.OpenFile(projectDefaultFile) 39 defer fc.Close() 40 defaultFiles = append(defaultFiles, fc) 41 } 42 43 configuration := config.Config{} 44 stepConfiguration, err := configuration.GetStepConfig( 45 map[string]interface{}{}, 46 "", 47 nil, 48 defaultFiles, 49 false, 50 filters, 51 stepMetadata, 52 map[string]interface{}{}, 53 "", 54 stepMetadata.Metadata.Name, 55 ) 56 checkError(err) 57 return stepConfiguration 58 } 59 60 // GenerateStepDocumentation generates step coding based on step configuration provided in yaml files 61 func GenerateStepDocumentation(metadataFiles []string, customDefaultFiles []string, docuHelperData DocuHelperData, azure bool) error { 62 includeAzure = azure 63 for key := range metadataFiles { 64 stepMetadata := readStepMetadata(metadataFiles[key], docuHelperData) 65 66 adjustDefaultValues(&stepMetadata) 67 68 stepConfiguration := readStepConfiguration(stepMetadata, customDefaultFiles, docuHelperData) 69 70 applyCustomDefaultValues(&stepMetadata, stepConfiguration) 71 72 adjustMandatoryFlags(&stepMetadata) 73 74 fmt.Print(" Generate documentation.. ") 75 if err := generateStepDocumentation(stepMetadata, docuHelperData); err != nil { 76 fmt.Println("") 77 fmt.Println(err) 78 } else { 79 fmt.Println("completed") 80 } 81 } 82 return nil 83 } 84 85 // generates the step documentation and replaces the template with the generated documentation 86 func generateStepDocumentation(stepData config.StepData, docuHelperData DocuHelperData) error { 87 //create the file path for the template and open it. 88 docTemplateFilePath := fmt.Sprintf("%v%v.md", docuHelperData.DocTemplatePath, stepData.Metadata.Name) 89 docTemplate, err := docuHelperData.OpenDocTemplateFile(docTemplateFilePath) 90 91 if docTemplate != nil { 92 defer docTemplate.Close() 93 } 94 // check if there is an error during opening the template (true : skip docu generation for this meta data file) 95 if err != nil { 96 return fmt.Errorf("error occurred: %v", err) 97 } 98 99 content := readAndAdjustTemplate(docTemplate) 100 if len(content) <= 0 { 101 return fmt.Errorf("error occurred: no content inside of the template") 102 } 103 104 // binding of functions and placeholder 105 tmpl, err := template.New("doc").Funcs(template.FuncMap{ 106 "StepName": createStepName, 107 "Description": createDescriptionSection, 108 "Parameters": createParametersSection, 109 }).Parse(content) 110 checkError(err) 111 112 // add secrets, context defaults to the step parameters 113 handleStepParameters(&stepData) 114 115 // write executed template data to the previously opened file 116 var docContent bytes.Buffer 117 err = tmpl.Execute(&docContent, &stepData) 118 checkError(err) 119 120 // overwrite existing file 121 err = docuHelperData.DocFileWriter(docTemplateFilePath, docContent.Bytes(), 644) 122 checkError(err) 123 124 return nil 125 } 126 127 func readContextInformation(contextDetailsPath string, contextDetails *config.StepData) { 128 contextDetailsFile, err := os.Open(contextDetailsPath) 129 checkError(err) 130 defer contextDetailsFile.Close() 131 132 err = contextDetails.ReadPipelineStepData(contextDetailsFile) 133 checkError(err) 134 } 135 136 func getContainerParameters(container config.Container, sidecar bool) map[string]interface{} { 137 containerParams := map[string]interface{}{} 138 139 if len(container.Command) > 0 { 140 containerParams[ifThenElse(sidecar, "sidecarCommand", "containerCommand")] = container.Command[0] 141 } 142 if len(container.EnvVars) > 0 { 143 containerParams[ifThenElse(sidecar, "sidecarEnvVars", "dockerEnvVars")] = config.EnvVarsAsMap(container.EnvVars) 144 } 145 containerParams[ifThenElse(sidecar, "sidecarImage", "dockerImage")] = container.Image 146 containerParams[ifThenElse(sidecar, "sidecarPullImage", "dockerPullImage")] = container.ImagePullPolicy != "Never" 147 if len(container.Name) > 0 { 148 containerParams[ifThenElse(sidecar, "sidecarName", "containerName")] = container.Name 149 containerParams["dockerName"] = container.Name 150 } 151 if len(container.Options) > 0 { 152 containerParams[ifThenElse(sidecar, "sidecarOptions", "dockerOptions")] = container.Options 153 } 154 if len(container.WorkingDir) > 0 { 155 containerParams[ifThenElse(sidecar, "sidecarWorkspace", "dockerWorkspace")] = container.WorkingDir 156 } 157 158 if sidecar { 159 if len(container.ReadyCommand) > 0 { 160 containerParams["sidecarReadyCommand"] = container.ReadyCommand 161 } 162 } else { 163 if len(container.Shell) > 0 { 164 containerParams["containerShell"] = container.Shell 165 } 166 } 167 168 //ToDo? add dockerVolumeBind, sidecarVolumeBind -> so far not part of config.Container 169 170 return containerParams 171 } 172 173 func handleStepParameters(stepData *config.StepData) { 174 175 stepParameterNames = stepData.GetParameterFilters().All 176 177 //add general options like script, verbose, etc. 178 //ToDo: add to context.yaml 179 appendGeneralOptionsToParameters(stepData) 180 181 //consolidate conditional parameters: 182 //- remove duplicate parameter entries 183 //- combine defaults (consider conditions) 184 consolidateConditionalParameters(stepData) 185 186 //get the context defaults 187 appendContextParameters(stepData) 188 189 //consolidate context defaults: 190 //- combine defaults (consider conditions) 191 consolidateContextDefaults(stepData) 192 193 setDefaultAndPossisbleValues(stepData) 194 } 195 196 func setDefaultAndPossisbleValues(stepData *config.StepData) { 197 for k, param := range stepData.Spec.Inputs.Parameters { 198 199 //fill default if not set 200 if param.Default == nil { 201 switch param.Type { 202 case "bool": 203 param.Default = false 204 case "int": 205 param.Default = 0 206 } 207 } 208 209 //add possible values where known for certain types 210 switch param.Type { 211 case "bool": 212 if param.PossibleValues == nil { 213 param.PossibleValues = []interface{}{true, false} 214 } 215 } 216 217 stepData.Spec.Inputs.Parameters[k] = param 218 } 219 } 220 221 func appendGeneralOptionsToParameters(stepData *config.StepData) { 222 script := config.StepParameters{ 223 Name: "script", Type: "Jenkins Script", Mandatory: true, 224 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.", 225 } 226 verbose := config.StepParameters{ 227 Name: "verbose", Type: "bool", Mandatory: false, Default: false, Scope: []string{"PARAMETERS", "GENERAL", "STEPS", "STAGES"}, 228 Description: "verbose output", 229 } 230 stepData.Spec.Inputs.Parameters = append(stepData.Spec.Inputs.Parameters, script, verbose) 231 } 232 233 // GenerateStepDocumentation generates pipeline stage documentation based on pipeline configuration provided in a yaml file 234 func GenerateStageDocumentation(stageMetadataPath, stageTargetPath, relativeStepsPath string, utils piperutils.FileUtils) error { 235 if len(stageTargetPath) == 0 { 236 return fmt.Errorf("stageTargetPath cannot be empty") 237 } 238 if len(stageMetadataPath) == 0 { 239 return fmt.Errorf("stageMetadataPath cannot be empty") 240 } 241 242 if err := utils.MkdirAll(stageTargetPath, 0777); err != nil { 243 return fmt.Errorf("failed to create directory '%v': %w", stageTargetPath, err) 244 } 245 246 stageMetadataContent, err := utils.FileRead(stageMetadataPath) 247 if err != nil { 248 return fmt.Errorf("failed to read stage metadata file '%v': %w", stageMetadataPath, err) 249 } 250 251 stageRunConfig := config.RunConfigV1{} 252 253 err = yaml.Unmarshal(stageMetadataContent, &stageRunConfig.PipelineConfig) 254 if err != nil { 255 return fmt.Errorf("format of configuration is invalid %q: %w", stageMetadataContent, err) 256 } 257 258 err = createPipelineDocumentation(&stageRunConfig, stageTargetPath, relativeStepsPath, utils) 259 if err != nil { 260 return fmt.Errorf("failed to create pipeline documentation: %w", err) 261 } 262 263 return nil 264 } 265 266 func createPipelineDocumentation(stageRunConfig *config.RunConfigV1, stageTargetPath, relativeStepsPath string, utils piperutils.FileUtils) error { 267 if err := createPipelineOverviewDocumentation(stageRunConfig, stageTargetPath, utils); err != nil { 268 return fmt.Errorf("failed to create pipeline overview: %w", err) 269 } 270 271 if err := createPipelineStageDocumentation(stageRunConfig, stageTargetPath, relativeStepsPath, utils); err != nil { 272 return fmt.Errorf("failed to create pipeline stage details: %w", err) 273 } 274 275 return nil 276 } 277 278 func createPipelineOverviewDocumentation(stageRunConfig *config.RunConfigV1, stageTargetPath string, utils piperutils.FileUtils) error { 279 overviewFileName := "overview.md" 280 overviewDoc := fmt.Sprintf("# %v\n\n", stageRunConfig.PipelineConfig.Metadata.DisplayName) 281 overviewDoc += fmt.Sprintf("%v\n\n", stageRunConfig.PipelineConfig.Metadata.Description) 282 overviewDoc += fmt.Sprintf("The %v comprises following stages\n\n", stageRunConfig.PipelineConfig.Metadata.DisplayName) 283 for _, stage := range stageRunConfig.PipelineConfig.Spec.Stages { 284 stageFilePath := filepath.Join(stageTargetPath, fmt.Sprintf("%v.md", stage.Name)) 285 overviewDoc += fmt.Sprintf("* [%v Stage](%v)\n", stage.DisplayName, stageFilePath) 286 } 287 overviewFilePath := filepath.Join(stageTargetPath, overviewFileName) 288 fmt.Println("writing file", overviewFilePath) 289 return utils.FileWrite(overviewFilePath, []byte(overviewDoc), 0666) 290 } 291 292 const stepConditionDetails = `!!! note "Step condition details" 293 There are currently several conditions which can be checked.<br />**Important: It will be sufficient that any one condition per step is met.** 294 295 * ` + "`" + `config` + "`" + `: Checks if a configuration parameter has a defined value. 296 * ` + "`" + `config key` + "`" + `: Checks if a defined configuration parameter is set. 297 * ` + "`" + `file pattern` + "`" + `: Checks if files according a defined pattern exist in the project. 298 * ` + "`" + `file pattern from config` + "`" + `: Checks if files according a pattern defined in the custom configuration exist in the project. 299 * ` + "`" + `npm script` + "`" + `: Checks if a npm script exists in one of the package.json files in the repositories. 300 301 ` 302 const overrulingStepActivation = `!!! note "Overruling step activation conditions" 303 It is possible to overrule the automatically detected step activation status. 304 305 * In case a step will be **active** you can add to your stage configuration ` + "`" + `<stepName>: false` + "`" + ` to explicitly **deactivate** the step. 306 * In case a step will be **inactive** you can add to your stage configuration ` + "`" + `<stepName>: true` + "`" + ` to explicitly **activate** the step. 307 308 ` 309 310 func createPipelineStageDocumentation(stageRunConfig *config.RunConfigV1, stageTargetPath, relativeStepsPath string, utils piperutils.FileUtils) error { 311 for _, stage := range stageRunConfig.PipelineConfig.Spec.Stages { 312 stageDoc := fmt.Sprintf("# %v\n\n", stage.DisplayName) 313 stageDoc += fmt.Sprintf("%v\n\n", stage.Description) 314 315 if len(stage.Steps) > 0 { 316 stageDoc += "## Stage Content\n\nThis stage comprises following steps which are activated depending on your use-case/configuration:\n\n" 317 318 for i, step := range stage.Steps { 319 if i == 0 { 320 stageDoc += "| step | step description |\n" 321 stageDoc += "| ---- | ---------------- |\n" 322 } 323 324 orchestratorBadges := "" 325 for _, orchestrator := range step.Orchestrators { 326 orchestratorBadges += getBadge(orchestrator) + " " 327 } 328 329 stageDoc += fmt.Sprintf("| [%v](%v/%v.md) | %v%v |\n", step.Name, relativeStepsPath, step.Name, orchestratorBadges, step.Description) 330 } 331 332 stageDoc += "\n" 333 334 stageDoc += "## Stage & Step Activation\n\nThis stage will be active in case one of following conditions are met:\n\n" 335 stageDoc += "* One of the steps is explicitly activated by using `<stepName>: true` in the stage configuration\n" 336 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" 337 338 stageDoc += stepConditionDetails 339 stageDoc += overrulingStepActivation 340 341 stageDoc += "Following conditions apply for activation of steps contained in the stage:\n\n" 342 343 stageDoc += "| step | active if one of following conditions is met |\n" 344 stageDoc += "| ---- | -------------------------------------------- |\n" 345 346 // add step condition details 347 for _, step := range stage.Steps { 348 stageDoc += fmt.Sprintf("| [%v](%v/%v.md) | %v |\n", step.Name, relativeStepsPath, step.Name, getStepConditionDetails(step)) 349 } 350 } 351 352 stageFilePath := filepath.Join(stageTargetPath, fmt.Sprintf("%v.md", stage.Name)) 353 fmt.Println("writing file", stageFilePath) 354 if err := utils.FileWrite(stageFilePath, []byte(stageDoc), 0666); err != nil { 355 return fmt.Errorf("failed to write stage file '%v': %w", stageFilePath, err) 356 } 357 } 358 return nil 359 } 360 361 func getBadge(orchestrator string) string { 362 orchestratorOnly := piperutils.Title(strings.ToLower(orchestrator)) + " only" 363 urlPath := &url.URL{Path: orchestratorOnly} 364 orchestratorOnlyString := urlPath.String() 365 366 return fmt.Sprintf("[![%v](https://img.shields.io/badge/-%v-yellowgreen)](#)", orchestratorOnly, orchestratorOnlyString) 367 } 368 369 func getStepConditionDetails(step config.Step) string { 370 stepConditions := "" 371 if step.Conditions == nil || len(step.Conditions) == 0 { 372 return "**active** by default - deactivate explicitly" 373 } 374 375 if len(step.Orchestrators) > 0 { 376 orchestratorBadges := "" 377 for _, orchestrator := range step.Orchestrators { 378 orchestratorBadges += getBadge(orchestrator) + " " 379 } 380 stepConditions = orchestratorBadges + "<br />" 381 } 382 383 for _, condition := range step.Conditions { 384 if condition.Config != nil && len(condition.Config) > 0 { 385 stepConditions += "<i>config:</i><ul>" 386 for param, activationValues := range condition.Config { 387 for _, activationValue := range activationValues { 388 stepConditions += fmt.Sprintf("<li>`%v`: `%v`</li>", param, activationValue) 389 } 390 // config condition only covers first entry 391 break 392 } 393 stepConditions += "</ul>" 394 continue 395 } 396 397 if len(condition.ConfigKey) > 0 { 398 stepConditions += fmt.Sprintf("<i>config key:</i> `%v`<br />", condition.ConfigKey) 399 continue 400 } 401 402 if len(condition.FilePattern) > 0 { 403 stepConditions += fmt.Sprintf("<i>file pattern:</i> `%v`<br />", condition.FilePattern) 404 continue 405 } 406 407 if len(condition.FilePatternFromConfig) > 0 { 408 stepConditions += fmt.Sprintf("<i>file pattern from config:</i> `%v`<br />", condition.FilePatternFromConfig) 409 continue 410 } 411 412 if len(condition.NpmScript) > 0 { 413 stepConditions += fmt.Sprintf("<i>npm script:</i> `%v`<br />", condition.NpmScript) 414 continue 415 } 416 417 if condition.Inactive { 418 stepConditions += "**inactive** by default - activate explicitly" 419 continue 420 } 421 } 422 423 return stepConditions 424 }