mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-14 11:03:09 +02:00
425 lines
15 KiB
Go
425 lines
15 KiB
Go
package generator
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/SAP/jenkins-library/pkg/config"
|
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
|
|
|
"github.com/ghodss/yaml"
|
|
)
|
|
|
|
// 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
|
|
var includeAzure bool
|
|
|
|
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,
|
|
stepMetadata,
|
|
map[string]interface{}{},
|
|
"",
|
|
stepMetadata.Metadata.Name,
|
|
)
|
|
checkError(err)
|
|
return stepConfiguration
|
|
}
|
|
|
|
// GenerateStepDocumentation generates step coding based on step configuration provided in yaml files
|
|
func GenerateStepDocumentation(metadataFiles []string, customDefaultFiles []string, docuHelperData DocuHelperData, azure bool) error {
|
|
includeAzure = azure
|
|
for key := range metadataFiles {
|
|
stepMetadata := readStepMetadata(metadataFiles[key], docuHelperData)
|
|
|
|
adjustDefaultValues(&stepMetadata)
|
|
|
|
stepConfiguration := readStepConfiguration(stepMetadata, customDefaultFiles, docuHelperData)
|
|
|
|
applyCustomDefaultValues(&stepMetadata, stepConfiguration)
|
|
|
|
adjustMandatoryFlags(&stepMetadata)
|
|
|
|
fmt.Print(" Generate documentation.. ")
|
|
if err := generateStepDocumentation(stepMetadata, docuHelperData); err != nil {
|
|
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 {
|
|
return fmt.Errorf("error occurred: %v", err)
|
|
}
|
|
|
|
content := readAndAdjustTemplate(docTemplate)
|
|
if len(content) <= 0 {
|
|
return fmt.Errorf("error occurred: no content inside of the template")
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
overviewDoc += fmt.Sprintf("The %v comprises following stages\n\n", stageRunConfig.PipelineConfig.Metadata.DisplayName)
|
|
for _, stage := range stageRunConfig.PipelineConfig.Spec.Stages {
|
|
stageFilePath := filepath.Join(stageTargetPath, fmt.Sprintf("%v.md", stage.Name))
|
|
overviewDoc += fmt.Sprintf("* [%v Stage](%v)\n", stage.DisplayName, stageFilePath)
|
|
}
|
|
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.**
|
|
|
|
* ` + "`" + `config` + "`" + `: Checks if a configuration parameter has a defined value.
|
|
* ` + "`" + `config key` + "`" + `: Checks if a defined configuration parameter is set.
|
|
* ` + "`" + `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)
|
|
|
|
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"
|
|
|
|
for i, step := range stage.Steps {
|
|
if i == 0 {
|
|
stageDoc += "| step | step description |\n"
|
|
stageDoc += "| ---- | ---------------- |\n"
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
stageDoc += "\n"
|
|
|
|
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"
|
|
|
|
stageDoc += stepConditionDetails
|
|
stageDoc += overrulingStepActivation
|
|
|
|
stageDoc += "Following conditions apply for activation of steps contained in the stage:\n\n"
|
|
|
|
stageDoc += "| step | active if one of following conditions is met |\n"
|
|
stageDoc += "| ---- | -------------------------------------------- |\n"
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
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 {
|
|
stepConditions += fmt.Sprintf("<i>config key:</i> `%v`<br />", condition.ConfigKey)
|
|
continue
|
|
}
|
|
|
|
if len(condition.FilePattern) > 0 {
|
|
stepConditions += fmt.Sprintf("<i>file pattern:</i> `%v`<br />", condition.FilePattern)
|
|
continue
|
|
}
|
|
|
|
if len(condition.FilePatternFromConfig) > 0 {
|
|
stepConditions += fmt.Sprintf("<i>file pattern from config:</i> `%v`<br />", condition.FilePatternFromConfig)
|
|
continue
|
|
}
|
|
|
|
if len(condition.NpmScript) > 0 {
|
|
stepConditions += fmt.Sprintf("<i>npm script:</i> `%v`<br />", condition.NpmScript)
|
|
continue
|
|
}
|
|
|
|
if condition.Inactive {
|
|
stepConditions += "**inactive** by default - activate explicitly"
|
|
continue
|
|
}
|
|
}
|
|
|
|
return stepConditions
|
|
}
|