github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/generator/helper/helper.go (about)

     1  package helper
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"strings"
    11  	"text/template"
    12  
    13  	"github.com/Masterminds/sprig"
    14  
    15  	"github.com/SAP/jenkins-library/pkg/config"
    16  	"github.com/SAP/jenkins-library/pkg/piperutils"
    17  )
    18  
    19  type stepInfo struct {
    20  	CobraCmdFuncName string
    21  	CreateCmdVar     string
    22  	ExportPrefix     string
    23  	FlagsFunc        string
    24  	Long             string
    25  	StepParameters   []config.StepParameters
    26  	StepAliases      []config.Alias
    27  	OSImport         bool
    28  	OutputResources  []map[string]string
    29  	Short            string
    30  	StepFunc         string
    31  	StepName         string
    32  	StepSecrets      []string
    33  	Containers       []config.Container
    34  	Sidecars         []config.Container
    35  	Outputs          config.StepOutputs
    36  	Resources        []config.StepResources
    37  	Secrets          []config.StepSecrets
    38  }
    39  
    40  // StepGoTemplate ...
    41  const stepGoTemplate = `// Code generated by piper's step-generator. DO NOT EDIT.
    42  
    43  package cmd
    44  
    45  {{ $reportsOutputExists := false -}}
    46  {{ $influxOutputExists := false -}}
    47  {{ $piperEnvironmentOutputExists := false -}}
    48  {{ if .OutputResources -}}
    49  	{{ range $notused, $oRes := .OutputResources -}}
    50  		{{ if eq (index $oRes "type") "reports" -}}{{ $reportsOutputExists = true -}}{{ end -}}
    51  		{{ if eq (index $oRes "type") "influx" -}}{{ $influxOutputExists = true -}}{{ end -}}
    52  		{{ if eq (index $oRes "type") "piperEnvironment" -}}{{ $piperEnvironmentOutputExists = true -}}{{ end -}}
    53  	{{ end -}}
    54  {{ end -}}
    55  
    56  import (
    57  	"fmt"
    58  	"os"
    59  	{{ if $reportsOutputExists -}}
    60  	"reflect"
    61  	"strings"
    62  	{{ end -}}
    63  	{{ if or $influxOutputExists $piperEnvironmentOutputExists -}}
    64  	"path/filepath"
    65  	{{ end -}}
    66  	"time"
    67  
    68  	{{ if .ExportPrefix -}}
    69  	{{ .ExportPrefix }} "github.com/ouraigua/jenkins-library/cmd"
    70  	{{ end -}}
    71  	"github.com/SAP/jenkins-library/pkg/config"
    72  	"github.com/SAP/jenkins-library/pkg/log"
    73  	{{ if $reportsOutputExists -}}
    74  	"github.com/bmatcuk/doublestar"
    75  	"github.com/SAP/jenkins-library/pkg/gcs"
    76  	{{ end -}}
    77  	{{ if or $influxOutputExists $piperEnvironmentOutputExists -}}
    78  	"github.com/SAP/jenkins-library/pkg/piperenv"
    79  	{{ end -}}
    80  	"github.com/SAP/jenkins-library/pkg/telemetry"
    81  	"github.com/SAP/jenkins-library/pkg/splunk"
    82  	"github.com/SAP/jenkins-library/pkg/validation"
    83  	"github.com/spf13/cobra"
    84  )
    85  
    86  type {{ .StepName }}Options struct {
    87  	{{- $names := list ""}}
    88  	{{- range $key, $value := uniqueName .StepParameters }}
    89  	{{ if ne (has $value.Name $names) true -}}
    90  	{{ $names | last }}{{ $value.Name | golangName }} {{ $value.Type }} ` + "`json:\"{{$value.Name}},omitempty\"" +
    91  	"{{ if or $value.PossibleValues $value.MandatoryIf}} validate:\"" +
    92  	"{{ if $value.PossibleValues }}possible-values={{ range $i,$a := $value.PossibleValues }}{{if gt $i 0 }} {{ end }}{{.}}{{ end }}{{ end }}" +
    93  	"{{ if and $value.PossibleValues $value.MandatoryIf }},{{ end }}" +
    94  	"{{ if $value.MandatoryIf }}required_if={{ range $i,$a := $value.MandatoryIf }}{{ if gt $i 0 }} {{ end }}{{ $a.Name | title }} {{ $a.Value }}{{ end }}{{ end }}" +
    95  	"\"{{ end }}`" + `
    96  	{{- else -}}
    97  	{{- $names = append $names $value.Name }} {{ end -}}
    98  	{{ end }}
    99  }
   100  
   101  {{ range $notused, $oRes := .OutputResources }}
   102  {{ index $oRes "def"}}
   103  {{ end }}
   104  
   105  // {{.CobraCmdFuncName}} {{.Short}}
   106  func {{.CobraCmdFuncName}}() *cobra.Command {
   107  	const STEP_NAME = {{ .StepName | quote }}
   108  
   109  	metadata := {{ .StepName }}Metadata()
   110  	var stepConfig {{.StepName}}Options
   111  	var startTime time.Time
   112  	{{- range $notused, $oRes := .OutputResources }}
   113  	var {{ index $oRes "name" }} {{ index $oRes "objectname" }}{{ end }}
   114  	var logCollector *log.CollectorHook
   115  	var splunkClient *splunk.Splunk
   116  	telemetryClient := &telemetry.Telemetry{}
   117  
   118  	var {{.CreateCmdVar}} = &cobra.Command{
   119  		Use:   STEP_NAME,
   120  		Short: {{.Short | quote }},
   121  		Long: {{ $tick := "` + "`" + `" }}{{ $tick }}{{.Long | longName }}{{ $tick }},
   122  		PreRunE: func(cmd *cobra.Command, _ []string) error {
   123  			startTime = time.Now()
   124  			log.SetStepName(STEP_NAME)
   125  			log.SetVerbose({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.Verbose)
   126  
   127  			{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.GitHubAccessTokens = {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}ResolveAccessTokens({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.GitHubTokens)
   128  
   129  			path, _ := os.Getwd()
   130  			fatalHook := &log.FatalHook{CorrelationID: {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID, Path: path}
   131  			log.RegisterHook(fatalHook)
   132  
   133  			err := {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
   134  			if err != nil {
   135  				log.SetErrorCategory(log.ErrorConfiguration)
   136  				return err
   137  			}
   138  
   139  			{{- range $key, $value := .StepSecrets }}
   140  			log.RegisterSecret(stepConfig.{{ $value | golangName  }}){{end}}
   141  
   142  			if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
   143  				sentryHook := log.NewSentryHook({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SentryConfig.Dsn, {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID)
   144  				log.RegisterHook(&sentryHook)
   145  			}
   146  
   147  			if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
   148  				splunkClient = &splunk.Splunk{}
   149  				logCollector = &log.CollectorHook{CorrelationID: {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID}
   150  				log.RegisterHook(logCollector)
   151  			}
   152  
   153  			if err = log.RegisterANSHookIfConfigured({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID); err != nil {
   154  				log.Entry().WithError(err).Warn("failed to set up SAP Alert Notification Service log hook")
   155  			}
   156  
   157  			validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages())
   158  			if err != nil {
   159  				return err
   160  			}
   161  			if err = validation.ValidateStruct(stepConfig); err != nil {
   162  				log.SetErrorCategory(log.ErrorConfiguration)
   163  				return err
   164  			}
   165  
   166  			return nil
   167  		},
   168  		Run: func(_ *cobra.Command, _ []string) {
   169  			stepTelemetryData := telemetry.CustomData{}
   170  			stepTelemetryData.ErrorCode = "1"
   171  			handler := func() {
   172  				{{- range $notused, $oRes := .OutputResources }}
   173  				{{ index $oRes "name" }}.persist(
   174  				{{- if eq (index $oRes "type") "reports" -}}stepConfig,
   175  					{{- if $.ExportPrefix}}{{ $.ExportPrefix }}.{{end}}GeneralConfig.GCPJsonKeyFilePath,
   176  					{{- if $.ExportPrefix}}{{ $.ExportPrefix }}.{{end}}GeneralConfig.GCSBucketId,
   177  					{{- if $.ExportPrefix}}{{ $.ExportPrefix }}.{{end}}GeneralConfig.GCSFolderPath,
   178  					{{- if $.ExportPrefix}}{{ $.ExportPrefix }}.{{end}}GeneralConfig.GCSSubFolder
   179  				{{- else -}}
   180  					{{if $.ExportPrefix}}{{ $.ExportPrefix }}.{{end}}GeneralConfig.EnvRootPath, {{ index $oRes "name" | quote }}{{- end -}}
   181  				){{- end }}
   182  				config.RemoveVaultSecretFiles()
   183  				stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
   184  				stepTelemetryData.ErrorCategory = log.GetErrorCategory().String()
   185  				stepTelemetryData.PiperCommitHash = {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GitCommit
   186  				telemetryClient.SetData(&stepTelemetryData)
   187  				telemetryClient.Send()
   188  				if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
   189  					splunkClient.Initialize({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID,
   190  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn,
   191  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Token,
   192  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Index,
   193  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.SendLogs)
   194  					splunkClient.Send(telemetryClient.GetData(), logCollector)
   195  				}
   196  				if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
   197  					splunkClient.Initialize({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID,
   198  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint,
   199  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.ProdCriblToken,
   200  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.ProdCriblIndex,
   201  					{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.SendLogs)
   202  					splunkClient.Send(telemetryClient.GetData(), logCollector)
   203  				}
   204  			}
   205  			log.DeferExitHandler(handler)
   206  			defer handler()
   207  			telemetryClient.Initialize({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.NoTelemetry, STEP_NAME)
   208  			{{.StepName}}(stepConfig, &stepTelemetryData{{ range $notused, $oRes := .OutputResources}}{{ if ne (index $oRes "type") "reports" }}, &{{ index $oRes "name" }}{{ end }}{{ end }})
   209  			stepTelemetryData.ErrorCode = "0"
   210  			log.Entry().Info("SUCCESS")
   211  		},
   212  	}
   213  
   214  	{{.FlagsFunc}}({{.CreateCmdVar}}, &stepConfig)
   215  	return {{.CreateCmdVar}}
   216  }
   217  
   218  func {{.FlagsFunc}}(cmd *cobra.Command, stepConfig *{{.StepName}}Options) {
   219  	{{- range $key, $value := uniqueName .StepParameters }}
   220  	{{ if isCLIParam $value.Type }}cmd.Flags().{{ $value.Type | flagType }}(&stepConfig.{{ $value.Name | golangName }}, {{ $value.Name | quote }}, {{ $value.Default }}, {{ $value.Description | quote }}){{end}}{{ end }}
   221  	{{- printf "\n" }}
   222  	{{- range $key, $value := .StepParameters }}
   223  	{{- if $value.Mandatory }}
   224  	cmd.MarkFlagRequired({{ $value.Name | quote }})
   225  	{{- end }}
   226  	{{- if $value.DeprecationMessage }}
   227  	cmd.Flags().MarkDeprecated({{ $value.Name | quote }}, {{ $value.DeprecationMessage | quote }})
   228  	{{- end }}
   229  	{{- end }}
   230  }
   231  
   232  {{ define "resourceRefs"}}
   233  							{{ "{" }}
   234  								Name: {{- .Name | quote }},
   235  								{{- if .Param }}
   236  								Param: {{ .Param | quote }},
   237  								{{- end }}
   238  								{{- if .Type }}
   239  								Type: {{ .Type | quote }},
   240  								{{- if .Default }}
   241  								Default: {{ .Default | quote }},
   242  								{{- end}}
   243  								{{- end }}
   244  							{{ "}" }},
   245  							{{- nindent 24 ""}}
   246  {{- end -}}
   247  
   248  // retrieve step metadata
   249  func {{ .StepName }}Metadata() config.StepData {
   250  	var theMetaData = config.StepData{
   251  		Metadata: config.StepMetadata{
   252  			Name:    {{ .StepName | quote }},
   253  			Aliases: []config.Alias{{ "{" }}{{ range $notused, $alias := .StepAliases }}{{ "{" }}Name: {{ $alias.Name | quote }}, Deprecated: {{ $alias.Deprecated }}{{ "}" }},{{ end }}{{ "}" }},
   254  			Description: {{ .Short | quote }},
   255  		},
   256  		Spec: config.StepSpec{
   257  			Inputs: config.StepInputs{
   258  				{{ if .Secrets -}}
   259  				Secrets: []config.StepSecrets{
   260  					{{- range $secrets := .Secrets }}
   261  					{
   262  						{{- if $secrets.Name -}} Name: {{ $secrets.Name | quote }},{{- end }}
   263  						{{- if $secrets.Description -}} Description: {{$secrets.Description | quote}},{{- end }}
   264  						{{- if $secrets.Type -}} Type: {{ $secrets.Type | quote }},{{- end }}
   265  						{{- if $secrets.Aliases -}} Aliases: []config.Alias{ {{- range $i, $a := $secrets.Aliases }} {Name: {{ $a.Name | quote }}, Deprecated: {{$a.Deprecated}}}, {{ end -}}  },{{- end }}
   266  					}, {{ end }}
   267  				},
   268  				{{ end -}}
   269  				{{ if .Resources -}}
   270  				Resources: []config.StepResources{
   271  					{{- range $resource := .Resources }}
   272  					{
   273  						{{- if $resource.Name -}} Name: {{ $resource.Name | quote }},{{- end }}
   274  						{{- if $resource.Description -}} Description: {{ $resource.Description | quote }},{{- end }}
   275  						{{- if $resource.Type -}} Type: {{ $resource.Type | quote }},{{- end }}
   276  						{{- if $resource.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $resource.Conditions }} {ConditionRef: {{ $cond.ConditionRef | quote }}, Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: {{ $p.Name | quote }}, Value: {{ $p.Value | quote }} }, {{end -}} } }, {{ end -}} },{{ end }}
   277  					},{{- end }}
   278  				},
   279  				{{ end -}}
   280  				Parameters: []config.StepParameters{
   281  					{{- range $key, $value := .StepParameters }}
   282  					{
   283  						Name:      {{ $value.Name | quote }},
   284  						ResourceRef: []config.ResourceReference{{ "{" }}{{ range $notused, $ref := $value.ResourceRef }}{{ template "resourceRefs" $ref }}{{ end }}{{ "}" }},
   285  						Scope:     []string{{ "{" }}{{ range $notused, $scope := $value.Scope }}{{ $scope | quote }},{{ end }}{{ "}" }},
   286  						Type:      {{ $value.Type | quote }},
   287  						Mandatory: {{ $value.Mandatory }},
   288  						Aliases:   []config.Alias{{ "{" }}{{ range $notused, $alias := $value.Aliases }}{{ "{" }}Name: {{ $alias.Name | quote }}{{ if $alias.Deprecated }}, Deprecated: {{$alias.Deprecated}}{{ end }}{{ "}" }},{{ end }}{{ "}" }},
   289  						{{ if $value.Default -}} Default:   {{ $value.Default }}, {{- end}}{{ if $value.Conditions }}
   290  						Conditions: []config.Condition{ {{- range $i, $cond := $value.Conditions }} {ConditionRef: {{ $cond.ConditionRef | quote }}, Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: {{ $p.Name | quote }}, Value: {{ $p.Value | quote }} }, {{end -}} } }, {{ end -}} },{{- end }}
   291  						{{- if $value.DeprecationMessage }}
   292  						DeprecationMessage: {{ $value.DeprecationMessage | quote }},
   293  						{{- end}}
   294  					},{{ end }}
   295  				},
   296  			},
   297  			{{ if .Containers -}}
   298  			Containers: []config.Container{
   299  				{{- range $container := .Containers }}
   300  				{
   301  					{{- if $container.Name -}} Name: {{ $container.Name | quote }},{{- end }}
   302  					{{- if $container.Image -}} Image: {{ $container.Image | quote }},{{- end }}
   303  					{{- if $container.EnvVars -}} EnvVars: []config.EnvVar{ {{- range $i, $env := $container.EnvVars }} {Name: {{ $env.Name | quote }}, Value: {{ $env.Value | quote }}}, {{ end -}}  },{{- end }}
   304  					{{- if $container.WorkingDir -}} WorkingDir: {{ $container.WorkingDir | quote }},{{- end }}
   305  					{{- if $container.Options -}} Options: []config.Option{ {{- range $i, $option := $container.Options }} {Name: {{ $option.Name | quote }}, Value: {{ $option.Value | quote }}}, {{ end -}} },{{ end }}
   306  					{{- if $container.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $container.Conditions }} {ConditionRef: {{ $cond.ConditionRef | quote }}, Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: {{ $p.Name | quote }}, Value: {{ $p.Value | quote }} }, {{end -}} } }, {{ end -}} },{{ end }}
   307  				}, {{ end }}
   308  			},
   309  			{{ end -}}
   310  			{{ if .Sidecars -}}
   311  			Sidecars: []config.Container{
   312  				{{- range $container := .Sidecars }}
   313  				{
   314  					{{- if $container.Name -}} Name: {{ $container.Name | quote }}, {{- end }}
   315  					{{- if $container.Image -}} Image: {{ $container.Image | quote }}, {{- end }}
   316  					{{- if $container.EnvVars -}} EnvVars: []config.EnvVar{ {{- range $i, $env := $container.EnvVars }} {Name: {{ $env.Name | quote }}, Value: {{ $env.Value | quote }}}, {{ end -}}  }, {{- end }}
   317  					{{- if $container.WorkingDir -}} WorkingDir: {{ $container.WorkingDir | quote }}, {{- end }}
   318  					{{- if $container.Options -}} Options: []config.Option{ {{- range $i, $option := $container.Options }} {Name: {{ $option.Name | quote }}, Value: {{ $option.Value | quote }}}, {{ end -}} }, {{- end }}
   319  					{{- if $container.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $container.Conditions }} {ConditionRef: {{ $cond.ConditionRef | quote }}, Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: {{ $p.Name | quote }}, Value: {{ $p.Value | quote }} }, {{end -}} } }, {{ end -}} }, {{- end }}
   320  				}, {{ end }}
   321  			},
   322  			{{ end -}}
   323  			{{- if .Outputs.Resources -}}
   324  			Outputs: config.StepOutputs{
   325  				Resources: []config.StepResources{
   326  					{{- range $res := .Outputs.Resources }}
   327  					{
   328  						{{ if $res.Name }}Name: {{ $res.Name | quote }}, {{- end }}
   329  						{{ if $res.Type }}Type: {{ $res.Type | quote }}, {{- end }}
   330  						{{ if $res.Parameters }}Parameters: []map[string]interface{}{ {{- end -}}
   331  						{{ range $i, $p := $res.Parameters }}
   332  							{{ if $p }}{ {{- end -}}
   333  							{{ if $p.name}}"name": {{ $p.name | quote }},{{ end -}}
   334  							{{ if $p.fields}}"fields": []map[string]string{ {{- range $j, $f := $p.fields}} {"name": {{ $f.name | quote }}}, {{end -}} },{{ end -}}
   335  							{{ if $p.tags}}"tags": []map[string]string{ {{- range $j, $t := $p.tags}} {"name": {{ $t.name | quote }}}, {{end -}} },{{ end -}}
   336  							{{ if $p.filePattern}}"filePattern": {{ $p.filePattern | quote }},{{ end -}}
   337  							{{ if $p.type}}"type": {{ $p.type | quote }},{{ end -}}
   338  							{{ if $p.subFolder}}"subFolder": {{ $p.subFolder | quote }},{{ end -}}
   339  							{{ if $p }}}, {{- end -}}
   340  						{{ end }}
   341  						{{ if $res.Parameters -}} }, {{- end }}
   342  						{{- if $res.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $res.Conditions }} {ConditionRef: {{ $cond.ConditionRef | quote }}, Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: {{ $p.Name | quote }}, Value: {{ $p.Value | quote }} }, {{end -}} } }, {{ end -}} },{{ end }}
   343  					}, {{- end }}
   344  				},
   345  			}, {{- end }}
   346  		},
   347  	}
   348  	return theMetaData
   349  }
   350  `
   351  
   352  // StepTestGoTemplate ...
   353  const stepTestGoTemplate = `//go:build unit
   354  // +build unit
   355  
   356  package cmd
   357  
   358  import (
   359  	"testing"
   360  
   361  	"github.com/stretchr/testify/assert"
   362  )
   363  
   364  func Test{{.CobraCmdFuncName}}(t *testing.T) {
   365  	t.Parallel()
   366  
   367  	testCmd := {{.CobraCmdFuncName}}()
   368  
   369  	// only high level testing performed - details are tested in step generation procedure
   370  	assert.Equal(t, {{ .StepName | quote }}, testCmd.Use, "command name incorrect")
   371  
   372  }
   373  `
   374  
   375  const stepGoImplementationTemplate = `package cmd
   376  import (
   377  	"fmt"
   378  	"github.com/SAP/jenkins-library/pkg/command"
   379  	"github.com/SAP/jenkins-library/pkg/log"
   380  	"github.com/SAP/jenkins-library/pkg/telemetry"
   381  	"github.com/SAP/jenkins-library/pkg/piperutils"
   382  )
   383  
   384  type {{.StepName}}Utils interface {
   385  	command.ExecRunner
   386  
   387  	FileExists(filename string) (bool, error)
   388  
   389  	// Add more methods here, or embed additional interfaces, or remove/replace as required.
   390  	// The {{.StepName}}Utils interface should be descriptive of your runtime dependencies,
   391  	// i.e. include everything you need to be able to mock in tests.
   392  	// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies.
   393  }
   394  
   395  type {{.StepName}}UtilsBundle struct {
   396  	*command.Command
   397  	*piperutils.Files
   398  
   399  	// Embed more structs as necessary to implement methods or interfaces you add to {{.StepName}}Utils.
   400  	// Structs embedded in this way must each have a unique set of methods attached.
   401  	// If there is no struct which implements the method you need, attach the method to
   402  	// {{.StepName}}UtilsBundle and forward to the implementation of the dependency.
   403  }
   404  
   405  func new{{.StepName | title}}Utils() {{.StepName}}Utils {
   406  	utils := {{.StepName}}UtilsBundle{
   407  		Command: &command.Command{},
   408  		Files:   &piperutils.Files{},
   409  	}
   410  	// Reroute command output to logging framework
   411  	utils.Stdout(log.Writer())
   412  	utils.Stderr(log.Writer())
   413  	return &utils
   414  }
   415  
   416  func {{.StepName}}(config {{ .StepName }}Options, telemetryData *telemetry.CustomData{{ range $notused, $oRes := .OutputResources}}, {{ index $oRes "name" }} *{{ index $oRes "objectname" }}{{ end }}) {
   417  	// Utils can be used wherever the command.ExecRunner interface is expected.
   418  	// It can also be used for example as a mavenExecRunner.
   419  	utils := new{{.StepName | title}}Utils()
   420  
   421  	// For HTTP calls import  piperhttp "github.com/SAP/jenkins-library/pkg/http"
   422  	// and use a  &piperhttp.Client{} in a custom system
   423  	// Example: step checkmarxExecuteScan.go
   424  
   425  	// Error situations should be bubbled up until they reach the line below which will then stop execution
   426  	// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
   427  	err := run{{.StepName | title}}(&config, telemetryData, utils{{ range $notused, $oRes := .OutputResources}}, &{{ index $oRes "name" }}{{ end }})
   428  	if err != nil {
   429  		log.Entry().WithError(err).Fatal("step execution failed")
   430  	}
   431  }
   432  
   433  func run{{.StepName | title}}(config *{{ .StepName }}Options, telemetryData *telemetry.CustomData, utils {{.StepName}}Utils{{ range $notused, $oRes := .OutputResources}}, {{ index $oRes "name" }} *{{ index $oRes "objectname" }} {{ end }}) error {
   434  	log.Entry().WithField("LogField", "Log field content").Info("This is just a demo for a simple step.")
   435  
   436  	// Example of calling methods from external dependencies directly on utils:
   437  	exists, err := utils.FileExists("file.txt")
   438  	if err != nil {
   439  		// It is good practice to set an error category.
   440  		// Most likely you want to do this at the place where enough context is known.
   441  		log.SetErrorCategory(log.ErrorConfiguration)
   442  		// Always wrap non-descriptive errors to enrich them with context for when they appear in the log:
   443  		return fmt.Errorf("failed to check for important file: %w", err)
   444  	}
   445  	if !exists {
   446  		log.SetErrorCategory(log.ErrorConfiguration)
   447  		return fmt.Errorf("cannot run without important file")
   448  	}
   449  
   450  	return nil
   451  }
   452  `
   453  
   454  const stepGoImplementationTestTemplate = `package cmd
   455  
   456  import (
   457  	"github.com/SAP/jenkins-library/pkg/mock"
   458  	"github.com/stretchr/testify/assert"
   459  	"testing"
   460  )
   461  
   462  type {{.StepName}}MockUtils struct {
   463  	*mock.ExecMockRunner
   464  	*mock.FilesMock
   465  }
   466  
   467  func new{{.StepName | title}}TestsUtils() {{.StepName}}MockUtils {
   468  	utils := {{.StepName}}MockUtils{
   469  		ExecMockRunner: &mock.ExecMockRunner{},
   470  		FilesMock:      &mock.FilesMock{},
   471  	}
   472  	return utils
   473  }
   474  
   475  func TestRun{{.StepName | title}}(t *testing.T) {
   476  	t.Parallel()
   477  
   478  	t.Run("happy path", func(t *testing.T) {
   479  		t.Parallel()
   480  		// init
   481  		config := {{.StepName}}Options{}
   482  
   483  		utils := new{{.StepName | title}}TestsUtils()
   484  		utils.AddFile("file.txt", []byte("dummy content"))
   485  
   486  		// test
   487  		err := run{{.StepName | title}}(&config, nil, utils)
   488  
   489  		// assert
   490  		assert.NoError(t, err)
   491  	})
   492  
   493  	t.Run("error path", func(t *testing.T) {
   494  		t.Parallel()
   495  		// init
   496  		config := {{.StepName}}Options{}
   497  
   498  		utils := new{{.StepName | title}}TestsUtils()
   499  
   500  		// test
   501  		err := run{{.StepName | title}}(&config, nil, utils)
   502  
   503  		// assert
   504  		assert.EqualError(t, err, "cannot run without important file")
   505  	})
   506  }
   507  `
   508  
   509  const metadataGeneratedFileName = "metadata_generated.go"
   510  const metadataGeneratedTemplate = `// Code generated by piper's step-generator. DO NOT EDIT.
   511  
   512  package cmd
   513  
   514  import "github.com/SAP/jenkins-library/pkg/config"
   515  
   516  // GetStepMetadata return a map with all the step metadata mapped to their stepName
   517  func GetAllStepMetadata() map[string]config.StepData {
   518  	return map[string]config.StepData{
   519  		{{range $stepName := .Steps }} {{ $stepName | quote }}: {{$stepName}}Metadata(),
   520  		{{end}}
   521  	}
   522  }
   523  `
   524  
   525  // ProcessMetaFiles generates step coding based on step configuration provided in yaml files
   526  func ProcessMetaFiles(metadataFiles []string, targetDir string, stepHelperData StepHelperData) error {
   527  
   528  	allSteps := struct{ Steps []string }{}
   529  	for key := range metadataFiles {
   530  
   531  		var stepData config.StepData
   532  
   533  		configFilePath := metadataFiles[key]
   534  
   535  		metadataFile, err := stepHelperData.OpenFile(configFilePath)
   536  		checkError(err)
   537  		defer metadataFile.Close()
   538  
   539  		fmt.Printf("Reading file %v\n", configFilePath)
   540  
   541  		err = stepData.ReadPipelineStepData(metadataFile)
   542  		checkError(err)
   543  
   544  		stepName := stepData.Metadata.Name
   545  		fmt.Printf("Step name: %v\n", stepName)
   546  		if stepName+".yaml" != filepath.Base(configFilePath) {
   547  			fmt.Printf("Expected file %s to have name %s.yaml (<stepName>.yaml)\n", configFilePath, filepath.Join(filepath.Dir(configFilePath), stepName))
   548  			os.Exit(1)
   549  		}
   550  		allSteps.Steps = append(allSteps.Steps, stepName)
   551  
   552  		for _, parameter := range stepData.Spec.Inputs.Parameters {
   553  			for _, mandatoryIfCase := range parameter.MandatoryIf {
   554  				if mandatoryIfCase.Name == "" || mandatoryIfCase.Value == "" {
   555  					return errors.New("invalid mandatoryIf option")
   556  				}
   557  			}
   558  		}
   559  
   560  		osImport := false
   561  		osImport, err = setDefaultParameters(&stepData)
   562  		checkError(err)
   563  
   564  		myStepInfo, err := getStepInfo(&stepData, osImport, stepHelperData.ExportPrefix)
   565  		checkError(err)
   566  
   567  		step := stepTemplate(myStepInfo, "step", stepGoTemplate)
   568  		err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v_generated.go", stepName)), step, 0644)
   569  		checkError(err)
   570  
   571  		test := stepTemplate(myStepInfo, "stepTest", stepTestGoTemplate)
   572  		err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v_generated_test.go", stepName)), test, 0644)
   573  		checkError(err)
   574  
   575  		exists, _ := piperutils.FileExists(filepath.Join(targetDir, fmt.Sprintf("%v.go", stepName)))
   576  		if !exists {
   577  			impl := stepImplementation(myStepInfo, "impl", stepGoImplementationTemplate)
   578  			err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v.go", stepName)), impl, 0644)
   579  			checkError(err)
   580  		}
   581  
   582  		exists, _ = piperutils.FileExists(filepath.Join(targetDir, fmt.Sprintf("%v_test.go", stepName)))
   583  		if !exists {
   584  			impl := stepImplementation(myStepInfo, "implTest", stepGoImplementationTestTemplate)
   585  			err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v_test.go", stepName)), impl, 0644)
   586  			checkError(err)
   587  		}
   588  	}
   589  
   590  	// expose metadata functions
   591  	code := generateCode(allSteps, "metadata", metadataGeneratedTemplate, sprig.HermeticTxtFuncMap())
   592  	err := stepHelperData.WriteFile(filepath.Join(targetDir, metadataGeneratedFileName), code, 0644)
   593  	checkError(err)
   594  
   595  	return nil
   596  }
   597  
   598  func setDefaultParameters(stepData *config.StepData) (bool, error) {
   599  	// ToDo: custom function for default handling, support all relevant parameter types
   600  	osImportRequired := false
   601  	for k, param := range stepData.Spec.Inputs.Parameters {
   602  
   603  		if param.Default == nil {
   604  			switch param.Type {
   605  			case "bool":
   606  				// ToDo: Check if default should be read from env
   607  				param.Default = "false"
   608  			case "int":
   609  				param.Default = "0"
   610  			case "string":
   611  				param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name)
   612  				osImportRequired = true
   613  			case "[]string":
   614  				// ToDo: Check if default should be read from env
   615  				param.Default = "[]string{}"
   616  			case "map[string]interface{}", "[]map[string]interface{}":
   617  				// Currently we don't need to set a default here since in this case the default
   618  				// is never used. Needs to be changed in case we enable cli parameter handling
   619  				// for that type.
   620  			default:
   621  				return false, fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
   622  			}
   623  		} else {
   624  			switch param.Type {
   625  			case "bool":
   626  				boolVal := "false"
   627  				if param.Default.(bool) == true {
   628  					boolVal = "true"
   629  				}
   630  				param.Default = boolVal
   631  			case "int":
   632  				param.Default = fmt.Sprintf("%v", param.Default)
   633  			case "string":
   634  				param.Default = fmt.Sprintf("`%v`", param.Default)
   635  			case "[]string":
   636  				param.Default = fmt.Sprintf("[]string{`%v`}", strings.Join(getStringSliceFromInterface(param.Default), "`, `"))
   637  			case "map[string]interface{}", "[]map[string]interface{}":
   638  				// Currently we don't need to set a default here since in this case the default
   639  				// is never used. Needs to be changed in case we enable cli parameter handling
   640  				// for that type.
   641  			default:
   642  				return false, fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
   643  			}
   644  		}
   645  
   646  		stepData.Spec.Inputs.Parameters[k] = param
   647  	}
   648  	return osImportRequired, nil
   649  }
   650  
   651  func getStepInfo(stepData *config.StepData, osImport bool, exportPrefix string) (stepInfo, error) {
   652  	oRes, err := getOutputResourceDetails(stepData)
   653  
   654  	return stepInfo{
   655  			StepName:         stepData.Metadata.Name,
   656  			CobraCmdFuncName: fmt.Sprintf("%vCommand", piperutils.Title(stepData.Metadata.Name)),
   657  			CreateCmdVar:     fmt.Sprintf("create%vCmd", piperutils.Title(stepData.Metadata.Name)),
   658  			Short:            stepData.Metadata.Description,
   659  			Long:             stepData.Metadata.LongDescription,
   660  			StepParameters:   stepData.Spec.Inputs.Parameters,
   661  			StepAliases:      stepData.Metadata.Aliases,
   662  			FlagsFunc:        fmt.Sprintf("add%vFlags", piperutils.Title(stepData.Metadata.Name)),
   663  			OSImport:         osImport,
   664  			OutputResources:  oRes,
   665  			ExportPrefix:     exportPrefix,
   666  			StepSecrets:      getSecretFields(stepData),
   667  			Containers:       stepData.Spec.Containers,
   668  			Sidecars:         stepData.Spec.Sidecars,
   669  			Outputs:          stepData.Spec.Outputs,
   670  			Resources:        stepData.Spec.Inputs.Resources,
   671  			Secrets:          stepData.Spec.Inputs.Secrets,
   672  		},
   673  		err
   674  }
   675  
   676  func getSecretFields(stepData *config.StepData) []string {
   677  	var secretFields []string
   678  
   679  	for _, parameter := range stepData.Spec.Inputs.Parameters {
   680  		if parameter.Secret {
   681  			secretFields = append(secretFields, parameter.Name)
   682  		}
   683  	}
   684  	return secretFields
   685  }
   686  
   687  func getOutputResourceDetails(stepData *config.StepData) ([]map[string]string, error) {
   688  	outputResources := []map[string]string{}
   689  
   690  	for _, res := range stepData.Spec.Outputs.Resources {
   691  		currentResource := map[string]string{}
   692  		currentResource["name"] = res.Name
   693  		currentResource["type"] = res.Type
   694  
   695  		switch res.Type {
   696  		case "piperEnvironment":
   697  			var envResource PiperEnvironmentResource
   698  			envResource.Name = res.Name
   699  			envResource.StepName = stepData.Metadata.Name
   700  			for _, param := range res.Parameters {
   701  				paramSections := strings.Split(fmt.Sprintf("%v", param["name"]), "/")
   702  				category := ""
   703  				name := paramSections[0]
   704  				if len(paramSections) > 1 {
   705  					name = strings.Join(paramSections[1:], "_")
   706  					category = paramSections[0]
   707  					if !contains(envResource.Categories, category) {
   708  						envResource.Categories = append(envResource.Categories, category)
   709  					}
   710  				}
   711  				envParam := PiperEnvironmentParameter{Category: category, Name: name, Type: fmt.Sprint(param["type"])}
   712  				envResource.Parameters = append(envResource.Parameters, envParam)
   713  			}
   714  			def, err := envResource.StructString()
   715  			if err != nil {
   716  				return outputResources, err
   717  			}
   718  			currentResource["def"] = def
   719  			currentResource["objectname"] = envResource.StructName()
   720  			outputResources = append(outputResources, currentResource)
   721  		case "influx":
   722  			var influxResource InfluxResource
   723  			influxResource.Name = res.Name
   724  			influxResource.StepName = stepData.Metadata.Name
   725  			for _, measurement := range res.Parameters {
   726  				influxMeasurement := InfluxMeasurement{Name: fmt.Sprintf("%v", measurement["name"])}
   727  				if fields, ok := measurement["fields"].([]interface{}); ok {
   728  					for _, field := range fields {
   729  						if fieldParams, ok := field.(map[string]interface{}); ok {
   730  							influxMeasurement.Fields = append(influxMeasurement.Fields, InfluxMetric{Name: fmt.Sprintf("%v", fieldParams["name"]), Type: fmt.Sprintf("%v", fieldParams["type"])})
   731  						}
   732  					}
   733  				}
   734  
   735  				if tags, ok := measurement["tags"].([]interface{}); ok {
   736  					for _, tag := range tags {
   737  						if tagParams, ok := tag.(map[string]interface{}); ok {
   738  							influxMeasurement.Tags = append(influxMeasurement.Tags, InfluxMetric{Name: fmt.Sprintf("%v", tagParams["name"])})
   739  						}
   740  					}
   741  				}
   742  				influxResource.Measurements = append(influxResource.Measurements, influxMeasurement)
   743  			}
   744  			def, err := influxResource.StructString()
   745  			if err != nil {
   746  				return outputResources, err
   747  			}
   748  			currentResource["def"] = def
   749  			currentResource["objectname"] = influxResource.StructName()
   750  			outputResources = append(outputResources, currentResource)
   751  		case "reports":
   752  			var reportsResource ReportsResource
   753  			reportsResource.Name = res.Name
   754  			reportsResource.StepName = stepData.Metadata.Name
   755  			for _, param := range res.Parameters {
   756  				filePattern, _ := param["filePattern"].(string)
   757  				paramRef, _ := param["paramRef"].(string)
   758  				if filePattern == "" && paramRef == "" {
   759  					return outputResources, errors.New("both filePattern and paramRef cannot be empty at the same time")
   760  				}
   761  				stepResultType, _ := param["type"].(string)
   762  				reportsParam := ReportsParameter{FilePattern: filePattern, ParamRef: paramRef, Type: stepResultType}
   763  				reportsResource.Parameters = append(reportsResource.Parameters, reportsParam)
   764  			}
   765  			def, err := reportsResource.StructString()
   766  			if err != nil {
   767  				return outputResources, err
   768  			}
   769  			currentResource["def"] = def
   770  			currentResource["objectname"] = reportsResource.StructName()
   771  			outputResources = append(outputResources, currentResource)
   772  		}
   773  	}
   774  
   775  	return outputResources, nil
   776  }
   777  
   778  // MetadataFiles provides a list of all step metadata files
   779  func MetadataFiles(sourceDirectory string) ([]string, error) {
   780  
   781  	var metadataFiles []string
   782  
   783  	err := filepath.Walk(sourceDirectory, func(path string, info os.FileInfo, err error) error {
   784  		if filepath.Ext(path) == ".yaml" {
   785  			metadataFiles = append(metadataFiles, path)
   786  		}
   787  		return nil
   788  	})
   789  	if err != nil {
   790  		return metadataFiles, nil
   791  	}
   792  	return metadataFiles, nil
   793  }
   794  
   795  func isCLIParam(myType string) bool {
   796  	return myType != "map[string]interface{}" && myType != "[]map[string]interface{}"
   797  }
   798  
   799  func stepTemplate(myStepInfo stepInfo, templateName, goTemplate string) []byte {
   800  	funcMap := sprig.HermeticTxtFuncMap()
   801  	funcMap["flagType"] = flagType
   802  	funcMap["golangName"] = GolangNameTitle
   803  	funcMap["title"] = piperutils.Title
   804  	funcMap["longName"] = longName
   805  	funcMap["uniqueName"] = mustUniqName
   806  	funcMap["isCLIParam"] = isCLIParam
   807  
   808  	return generateCode(myStepInfo, templateName, goTemplate, funcMap)
   809  }
   810  
   811  func stepImplementation(myStepInfo stepInfo, templateName, goTemplate string) []byte {
   812  	funcMap := sprig.HermeticTxtFuncMap()
   813  	funcMap["title"] = piperutils.Title
   814  	funcMap["uniqueName"] = mustUniqName
   815  
   816  	return generateCode(myStepInfo, templateName, goTemplate, funcMap)
   817  }
   818  
   819  func generateCode(dataObject interface{}, templateName, goTemplate string, funcMap template.FuncMap) []byte {
   820  	tmpl, err := template.New(templateName).Funcs(funcMap).Parse(goTemplate)
   821  	checkError(err)
   822  
   823  	var generatedCode bytes.Buffer
   824  	err = tmpl.Execute(&generatedCode, dataObject)
   825  	checkError(err)
   826  
   827  	return generatedCode.Bytes()
   828  }
   829  
   830  func longName(long string) string {
   831  	l := strings.ReplaceAll(long, "`", "` + \"`\" + `")
   832  	l = strings.TrimSpace(l)
   833  	return l
   834  }
   835  
   836  func resourceFieldType(fieldType string) string {
   837  	// TODO: clarify why fields are initialized with <nil> and tags are initialized with ''
   838  	if len(fieldType) == 0 || fieldType == "<nil>" {
   839  		return "string"
   840  	}
   841  	return fieldType
   842  }
   843  
   844  func golangName(name string) string {
   845  	properName := strings.Replace(name, "Api", "API", -1)
   846  	properName = strings.Replace(properName, "api", "API", -1)
   847  	properName = strings.Replace(properName, "Url", "URL", -1)
   848  	properName = strings.Replace(properName, "Id", "ID", -1)
   849  	properName = strings.Replace(properName, "Json", "JSON", -1)
   850  	properName = strings.Replace(properName, "json", "JSON", -1)
   851  	properName = strings.Replace(properName, "Tls", "TLS", -1)
   852  	return properName
   853  }
   854  
   855  // GolangNameTitle returns name in title case with abbriviations in capital (API, URL, ID, JSON, TLS)
   856  func GolangNameTitle(name string) string {
   857  	return piperutils.Title(golangName(name))
   858  }
   859  
   860  func flagType(paramType string) string {
   861  	var theFlagType string
   862  	switch paramType {
   863  	case "bool":
   864  		theFlagType = "BoolVar"
   865  	case "int":
   866  		theFlagType = "IntVar"
   867  	case "string":
   868  		theFlagType = "StringVar"
   869  	case "[]string":
   870  		theFlagType = "StringSliceVar"
   871  	default:
   872  		fmt.Printf("Meta data type not set or not known: '%v'\n", paramType)
   873  		os.Exit(1)
   874  	}
   875  	return theFlagType
   876  }
   877  
   878  func getStringSliceFromInterface(iSlice interface{}) []string {
   879  	s := []string{}
   880  
   881  	t, ok := iSlice.([]interface{})
   882  	if ok {
   883  		for _, v := range t {
   884  			s = append(s, fmt.Sprintf("%v", v))
   885  		}
   886  	} else {
   887  		s = append(s, fmt.Sprintf("%v", iSlice))
   888  	}
   889  
   890  	return s
   891  }
   892  
   893  func mustUniqName(list []config.StepParameters) ([]config.StepParameters, error) {
   894  	tp := reflect.TypeOf(list).Kind()
   895  	switch tp {
   896  	case reflect.Slice, reflect.Array:
   897  		l2 := reflect.ValueOf(list)
   898  
   899  		l := l2.Len()
   900  		names := []string{}
   901  		dest := []config.StepParameters{}
   902  		var item config.StepParameters
   903  		for i := 0; i < l; i++ {
   904  			item = l2.Index(i).Interface().(config.StepParameters)
   905  			if !piperutils.ContainsString(names, item.Name) {
   906  				names = append(names, item.Name)
   907  				dest = append(dest, item)
   908  			}
   909  		}
   910  
   911  		return dest, nil
   912  	default:
   913  		return nil, fmt.Errorf("Cannot find uniq on type %s", tp)
   914  	}
   915  }