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 }