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>&nbsp;`%v`<br />", condition.ConfigKey)
   399  			continue
   400  		}
   401  
   402  		if len(condition.FilePattern) > 0 {
   403  			stepConditions += fmt.Sprintf("<i>file pattern:</i>&nbsp;`%v`<br />", condition.FilePattern)
   404  			continue
   405  		}
   406  
   407  		if len(condition.FilePatternFromConfig) > 0 {
   408  			stepConditions += fmt.Sprintf("<i>file pattern from config:</i>&nbsp;`%v`<br />", condition.FilePatternFromConfig)
   409  			continue
   410  		}
   411  
   412  		if len(condition.NpmScript) > 0 {
   413  			stepConditions += fmt.Sprintf("<i>npm script:</i>&nbsp;`%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  }