1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/pkg/generator/helper/helper.go

812 lines
29 KiB
Go
Raw Normal View History

2019-11-08 16:35:11 +02:00
package helper
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
2019-11-08 16:35:11 +02:00
"strings"
"text/template"
"github.com/Masterminds/sprig"
2019-11-08 16:35:11 +02:00
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/piperutils"
2019-11-08 16:35:11 +02:00
)
type stepInfo struct {
CobraCmdFuncName string
CreateCmdVar string
ExportPrefix string
FlagsFunc string
Long string
StepParameters []config.StepParameters
StepAliases []config.Alias
2019-11-08 16:35:11 +02:00
OSImport bool
OutputResources []map[string]string
2019-11-08 16:35:11 +02:00
Short string
StepFunc string
StepName string
2020-04-16 14:37:45 +02:00
StepSecrets []string
Containers []config.Container
Sidecars []config.Container
Outputs config.StepOutputs
Resources []config.StepResources
Secrets []config.StepSecrets
2019-11-08 16:35:11 +02:00
}
//StepGoTemplate ...
const stepGoTemplate = `// Code generated by piper's step-generator. DO NOT EDIT.
package cmd
2019-11-08 16:35:11 +02:00
import (
"fmt"
"os"
{{ if .OutputResources -}}
"path/filepath"
{{ end -}}
"time"
2019-11-08 16:35:11 +02:00
{{ if .ExportPrefix -}}
{{ .ExportPrefix }} "github.com/SAP/jenkins-library/cmd"
{{ end -}}
2019-11-08 16:35:11 +02:00
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
{{ if .OutputResources -}}
"github.com/SAP/jenkins-library/pkg/piperenv"
{{ end -}}
2020-01-29 14:17:54 +02:00
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/splunk"
2019-11-08 16:35:11 +02:00
"github.com/spf13/cobra"
)
type {{ .StepName }}Options struct {
{{- $names := list ""}}
{{- range $key, $value := uniqueName .StepParameters }}
{{ if ne (has $value.Name $names) true -}}
{{ $names | last }}{{ $value.Name | golangName }} {{ $value.Type }} ` + "`json:\"{{$value.Name}},omitempty\"`" + `
{{- else -}}
{{- $names = append $names $value.Name }} {{ end -}}
{{ end }}
2019-11-08 16:35:11 +02:00
}
{{ range $notused, $oRes := .OutputResources }}
{{ index $oRes "def"}}
{{ end }}
2019-11-08 16:35:11 +02:00
// {{.CobraCmdFuncName}} {{.Short}}
func {{.CobraCmdFuncName}}() *cobra.Command {
const STEP_NAME = "{{ .StepName }}"
2019-11-08 16:35:11 +02:00
metadata := {{ .StepName }}Metadata()
var stepConfig {{.StepName}}Options
var startTime time.Time
{{- range $notused, $oRes := .OutputResources }}
var {{ index $oRes "name" }} {{ index $oRes "objectname" }}{{ end }}
var logCollector *log.CollectorHook
2019-11-08 16:35:11 +02:00
var {{.CreateCmdVar}} = &cobra.Command{
Use: STEP_NAME,
2019-11-08 16:35:11 +02:00
Short: "{{.Short}}",
Long: {{ $tick := "` + "`" + `" }}{{ $tick }}{{.Long | longName }}{{ $tick }},
PreRunE: func(cmd *cobra.Command, _ []string) error {
startTime = time.Now()
log.SetStepName(STEP_NAME)
2019-11-08 16:35:11 +02:00
log.SetVerbose({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.Verbose)
path, _ := os.Getwd()
2020-05-08 10:41:01 +02:00
fatalHook := &log.FatalHook{CorrelationID: {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID, Path: path}
log.RegisterHook(fatalHook)
err := {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
2020-04-16 14:37:45 +02:00
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
2020-04-16 14:37:45 +02:00
return err
}
{{- range $key, $value := .StepSecrets }}
log.RegisterSecret(stepConfig.{{ $value | golangName }}){{end}}
2020-05-08 10:41:01 +02:00
if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
sentryHook := log.NewSentryHook({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SentryConfig.Dsn, {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID)
log.RegisterHook(&sentryHook)
}
if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
logCollector = &log.CollectorHook{CorrelationID: {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID}
log.RegisterHook(logCollector)
}
2020-04-16 14:37:45 +02:00
return nil
2019-11-08 16:35:11 +02:00
},
Run: func(_ *cobra.Command, _ []string) {
telemetryData := telemetry.CustomData{}
telemetryData.ErrorCode = "1"
handler := func() {
config.RemoveVaultSecretFiles()
{{- range $notused, $oRes := .OutputResources }}
{{ index $oRes "name" }}.persist({{if $.ExportPrefix}}{{ $.ExportPrefix }}.{{end}}GeneralConfig.EnvRootPath, "{{ index $oRes "name" }}"){{ end }}
telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
telemetryData.ErrorCategory = log.GetErrorCategory().String()
telemetry.Send(&telemetryData)
if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
splunk.Send(&telemetryData, logCollector)
}
}
log.DeferExitHandler(handler)
defer handler()
telemetry.Initialize({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.NoTelemetry, STEP_NAME)
if len({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
splunk.Initialize({{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.CorrelationID,
{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Dsn,
{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Token,
{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.Index,
{{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}GeneralConfig.HookConfig.SplunkConfig.SendLogs)
}
{{.StepName}}(stepConfig, &telemetryData{{ range $notused, $oRes := .OutputResources}}, &{{ index $oRes "name" }}{{ end }})
telemetryData.ErrorCode = "0"
log.Entry().Info("SUCCESS")
2019-11-08 16:35:11 +02:00
},
}
{{.FlagsFunc}}({{.CreateCmdVar}}, &stepConfig)
2019-11-08 16:35:11 +02:00
return {{.CreateCmdVar}}
}
func {{.FlagsFunc}}(cmd *cobra.Command, stepConfig *{{.StepName}}Options) {
{{- range $key, $value := uniqueName .StepParameters }}
generator helper: support for map[string]interface{} (#2232) Up to now the code generator is not able to handle the type map[string]interface{} which is important for nested configurations. With that change we support such nested configuration. Fo now parameters with a map type are not supported via command line parameters. Those parameters are simply ommitted. But with this change is it possible to read such nested structures from the pipeline configuration (.pipeline/config.yml). As a next step we can discuss if we would like to support such values also via command line parameters. One possible approach could be ``` ./piper <command> -myParam key1=val1 --myParam key2=val2 ``` which gets finally collected inside our map: ``` map["key1"] = "val1" map["key2"] = "val2" ``` This is of course hard to do for deeper nestings. In that case providing a pointer to a file might be more suitable. In that context we need to consider how to - declare the default values for map like parameters in our metadata files. - deal with the different types we have for the parameter itself wrt the yaml like config on the one hand and on the level of the command line parameters on the other hand. Maybe for that we have to extend the metadata format (e.g. describe an alternate type receiving the values from the command line, like []string. With that approach values for simple nested (... not deep nested) params can be provided like described above, it would be possible to represent these parameters for the command line parser as string slice entries like "[]string{key1=val1, key2=val2". These parameters needs in this case transformed "by us" into the map we use further down the road. In case we agree in principle on an approach as outlined here we should adjust the golden files reflecting this use case.
2020-11-23 08:55:20 +02:00
{{ if isCLIParam $value.Type }}cmd.Flags().{{ $value.Type | flagType }}(&stepConfig.{{ $value.Name | golangName }}, "{{ $value.Name }}", {{ $value.Default }}, "{{ $value.Description }}"){{end}}{{ end }}
2019-11-08 16:35:11 +02:00
{{- printf "\n" }}
{{- range $key, $value := .StepParameters }}{{ if $value.Mandatory }}
2019-11-08 16:35:11 +02:00
cmd.MarkFlagRequired("{{ $value.Name }}"){{ end }}{{ end }}
}
{{ define "resourceRefs"}}
{{ "{" }}
Name: "{{- .Name }}",
{{- if .Param }}
Param: "{{ .Param }}",
{{- end }}
{{- if gt (len .Paths) 0 }}
Paths: []string{{ "{" }}{{ range $_, $path := .Paths }}"{{$path}}",{{ end }}{{"}"}},
{{- end }}
{{- if .Type }}
Type: "{{ .Type }}",
{{- end }}
{{ "}" }},
{{- nindent 24 ""}}
{{- end -}}
2019-11-08 16:35:11 +02:00
// retrieve step metadata
func {{ .StepName }}Metadata() config.StepData {
var theMetaData = config.StepData{
Metadata: config.StepMetadata{
Name: "{{ .StepName }}",
Aliases: []config.Alias{{ "{" }}{{ range $notused, $alias := .StepAliases }}{{ "{" }}Name: "{{ $alias.Name }}", Deprecated: {{ $alias.Deprecated }}{{ "}" }},{{ end }}{{ "}" }},
Description: "{{ .Short }}",
},
2019-11-08 16:35:11 +02:00
Spec: config.StepSpec{
Inputs: config.StepInputs{
{{ if .Secrets -}}
Secrets: []config.StepSecrets{
{{- range $secrets := .Secrets }}
{
{{- if $secrets.Name -}} Name: "{{$secrets.Name}}",{{- end }}
{{- if $secrets.Description -}} Description: "{{$secrets.Description}}",{{- end }}
{{- if $secrets.Type -}} Type: "{{$secrets.Type}}",{{- end }}
{{- if $secrets.Aliases -}} Aliases: []config.Alias{ {{- range $i, $a := $secrets.Aliases }} {Name: "{{$a.Name}}", Deprecated: {{$a.Deprecated}}}, {{ end -}} },{{- end }}
}, {{ end }}
},
{{ end -}}
{{ if .Resources -}}
Resources: []config.StepResources{
{{- range $resource := .Resources }}
{
{{- if $resource.Name -}} Name: "{{$resource.Name}}",{{- end }}
{{- if $resource.Description -}} Description: "{{$resource.Description}}",{{- end }}
{{- if $resource.Type -}} Type: "{{$resource.Type}}",{{- end }}
{{- if $resource.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $resource.Conditions }} {ConditionRef: "{{$cond.ConditionRef}}", Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: "{{$p.Name}}", Value: "{{$p.Value}}" }, {{end -}} } }, {{ end -}} },{{ end }}
},{{- end }}
},
{{ end -}}
2019-11-08 16:35:11 +02:00
Parameters: []config.StepParameters{
{{- range $key, $value := .StepParameters }}
2019-11-08 16:35:11 +02:00
{
Name: "{{ $value.Name }}",
ResourceRef: []config.ResourceReference{{ "{" }}{{ range $notused, $ref := $value.ResourceRef }}{{ template "resourceRefs" $ref }}{{ end }}{{ "}" }},
2019-11-08 16:35:11 +02:00
Scope: []string{{ "{" }}{{ range $notused, $scope := $value.Scope }}"{{ $scope }}",{{ end }}{{ "}" }},
Type: "{{ $value.Type }}",
Mandatory: {{ $value.Mandatory }},
Aliases: []config.Alias{{ "{" }}{{ range $notused, $alias := $value.Aliases }}{{ "{" }}Name: "{{ $alias.Name }}"{{ "}" }},{{ end }}{{ "}" }},
{{ if $value.Default -}} Default: {{ $value.Default }}, {{- end}}{{ if $value.Conditions }}
Conditions: []config.Condition{ {{- range $i, $cond := $value.Conditions }} {ConditionRef: "{{$cond.ConditionRef}}", Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: "{{$p.Name}}", Value: "{{$p.Value}}" }, {{end -}} } }, {{ end -}} },{{- end }}
2019-11-08 16:35:11 +02:00
},{{ end }}
},
},
{{ if .Containers -}}
Containers: []config.Container{
{{- range $container := .Containers }}
{
{{- if $container.Name -}} Name: "{{$container.Name}}",{{- end }}
{{- if $container.Image -}} Image: "{{$container.Image}}",{{- end }}
{{- if $container.EnvVars -}} EnvVars: []config.EnvVar{ {{- range $i, $env := $container.EnvVars }} {Name: "{{$env.Name}}", Value: "{{$env.Value}}"}, {{ end -}} },{{- end }}
{{- if $container.WorkingDir -}} WorkingDir: "{{$container.WorkingDir}}",{{- end }}
{{- if $container.Options -}} Options: []config.Option{ {{- range $i, $option := $container.Options }} {Name: "{{$option.Name}}", Value: "{{$option.Value}}"}, {{ end -}} },{{ end }}
{{- if $container.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $container.Conditions }} {ConditionRef: "{{$cond.ConditionRef}}", Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: "{{$p.Name}}", Value: "{{$p.Value}}" }, {{end -}} } }, {{ end -}} },{{ end }}
}, {{ end }}
},
{{ end -}}
{{ if .Sidecars -}}
Sidecars: []config.Container{
{{- range $container := .Sidecars }}
{
{{- if $container.Name -}} Name: "{{$container.Name}}", {{- end }}
{{- if $container.Image -}} Image: "{{$container.Image}}", {{- end }}
{{- if $container.EnvVars -}} EnvVars: []config.EnvVar{ {{- range $i, $env := $container.EnvVars }} {Name: "{{$env.Name}}", Value: "{{$env.Value}}"}, {{ end -}} }, {{- end }}
{{- if $container.WorkingDir -}} WorkingDir: "{{$container.WorkingDir}}", {{- end }}
{{- if $container.Options -}} Options: []config.Option{ {{- range $i, $option := $container.Options }} {Name: "{{$option.Name}}", Value: "{{$option.Value}}"}, {{ end -}} }, {{- end }}
{{- if $container.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $container.Conditions }} {ConditionRef: "{{$cond.ConditionRef}}", Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: "{{$p.Name}}", Value: "{{$p.Value}}" }, {{end -}} } }, {{ end -}} }, {{- end }}
}, {{ end }}
},
{{ end -}}
{{- if .Outputs.Resources -}}
Outputs: config.StepOutputs{
Resources: []config.StepResources{
{{- range $res := .Outputs.Resources }}
{
{{ if $res.Name }}Name: "{{$res.Name}}", {{- end }}
{{ if $res.Type }}Type: "{{$res.Type}}", {{- end }}
{{ if $res.Parameters }}Parameters: []map[string]interface{}{ {{- end -}}
{{ range $i, $p := $res.Parameters }}
{{ if $p.name}}{"Name": "{{$p.name}}"},{{ end -}}
{{ if $p.fields}}{"fields": []map[string]string{ {{- range $j, $f := $p.fields}} {"name": "{{$f.name}}"}, {{end -}} } },{{ end -}}
{{ if $p.tags}}{"tags": []map[string]string{ {{- range $j, $t := $p.tags}} {"name": "{{$t.name}}"}, {{end -}} } },{{ end -}}
{{ end }}
{{ if $res.Parameters -}} }, {{- end }}
{{- if $res.Conditions -}} Conditions: []config.Condition{ {{- range $i, $cond := $res.Conditions }} {ConditionRef: "{{$cond.ConditionRef}}", Params: []config.Param{ {{- range $j, $p := $cond.Params}} { Name: "{{$p.Name}}", Value: "{{$p.Value}}" }, {{end -}} } }, {{ end -}} },{{ end }}
}, {{- end }}
},
}, {{- end }}
2019-11-08 16:35:11 +02:00
},
}
return theMetaData
}
`
//StepTestGoTemplate ...
const stepTestGoTemplate = `package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test{{.CobraCmdFuncName}}(t *testing.T) {
t.Parallel()
2019-11-08 16:35:11 +02:00
testCmd := {{.CobraCmdFuncName}}()
// only high level testing performed - details are tested in step generation procedure
assert.Equal(t, "{{ .StepName }}", testCmd.Use, "command name incorrect")
2019-11-08 16:35:11 +02:00
}
`
const stepGoImplementationTemplate = `package cmd
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/piperutils"
)
type {{.StepName}}Utils interface {
command.ExecRunner
FileExists(filename string) (bool, error)
// Add more methods here, or embed additional interfaces, or remove/replace as required.
// The {{.StepName}}Utils interface should be descriptive of your runtime dependencies,
// i.e. include everything you need to be able to mock in tests.
// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies.
}
type {{.StepName}}UtilsBundle struct {
*command.Command
*piperutils.Files
// Embed more structs as necessary to implement methods or interfaces you add to {{.StepName}}Utils.
// Structs embedded in this way must each have a unique set of methods attached.
// If there is no struct which implements the method you need, attach the method to
// {{.StepName}}UtilsBundle and forward to the implementation of the dependency.
}
func new{{.StepName | title}}Utils() {{.StepName}}Utils {
utils := {{.StepName}}UtilsBundle{
Command: &command.Command{},
Files: &piperutils.Files{},
}
// Reroute command output to logging framework
utils.Stdout(log.Writer())
utils.Stderr(log.Writer())
return &utils
}
func {{.StepName}}(config {{ .StepName }}Options, telemetryData *telemetry.CustomData{{ range $notused, $oRes := .OutputResources}}, {{ index $oRes "name" }} *{{ index $oRes "objectname" }}{{ end }}) {
// Utils can be used wherever the command.ExecRunner interface is expected.
// It can also be used for example as a mavenExecRunner.
utils := new{{.StepName | title}}Utils()
// For HTTP calls import piperhttp "github.com/SAP/jenkins-library/pkg/http"
// and use a &piperhttp.Client{} in a custom system
// Example: step checkmarxExecuteScan.go
// Error situations should be bubbled up until they reach the line below which will then stop execution
// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
err := run{{.StepName | title}}(&config, telemetryData, utils{{ range $notused, $oRes := .OutputResources}}, &{{ index $oRes "name" }}{{ end }})
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
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 {
log.Entry().WithField("LogField", "Log field content").Info("This is just a demo for a simple step.")
// Example of calling methods from external dependencies directly on utils:
exists, err := utils.FileExists("file.txt")
if err != nil {
// It is good practice to set an error category.
// Most likely you want to do this at the place where enough context is known.
log.SetErrorCategory(log.ErrorConfiguration)
// Always wrap non-descriptive errors to enrich them with context for when they appear in the log:
return fmt.Errorf("failed to check for important file: %w", err)
}
if !exists {
log.SetErrorCategory(log.ErrorConfiguration)
return fmt.Errorf("cannot run without important file")
}
return nil
}
`
const stepGoImplementationTestTemplate = `package cmd
import (
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"testing"
)
type {{.StepName}}MockUtils struct {
*mock.ExecMockRunner
*mock.FilesMock
}
func new{{.StepName | title}}TestsUtils() {{.StepName}}MockUtils {
utils := {{.StepName}}MockUtils{
ExecMockRunner: &mock.ExecMockRunner{},
FilesMock: &mock.FilesMock{},
}
return utils
}
func TestRun{{.StepName | title}}(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
t.Parallel()
// init
config := {{.StepName}}Options{}
utils := new{{.StepName | title}}TestsUtils()
utils.AddFile("file.txt", []byte("dummy content"))
// test
err := run{{.StepName | title}}(&config, nil, utils)
// assert
assert.NoError(t, err)
})
t.Run("error path", func(t *testing.T) {
t.Parallel()
// init
config := {{.StepName}}Options{}
utils := new{{.StepName | title}}TestsUtils()
// test
err := run{{.StepName | title}}(&config, nil, utils)
// assert
assert.EqualError(t, err, "cannot run without important file")
})
}
`
const metadataGeneratedFileName = "metadata_generated.go"
const metadataGeneratedTemplate = `// Code generated by piper's step-generator. DO NOT EDIT.
package cmd
import "github.com/SAP/jenkins-library/pkg/config"
// GetStepMetadata return a map with all the step metadata mapped to their stepName
func GetAllStepMetadata() map[string]config.StepData {
return map[string]config.StepData{
{{range $stepName := .Steps }} "{{$stepName}}": {{$stepName}}Metadata(),
{{end}}
}
}
`
2019-11-08 16:35:11 +02:00
// ProcessMetaFiles generates step coding based on step configuration provided in yaml files
func ProcessMetaFiles(metadataFiles []string, targetDir string, stepHelperData StepHelperData) error {
allSteps := struct{ Steps []string }{}
2019-11-08 16:35:11 +02:00
for key := range metadataFiles {
var stepData config.StepData
configFilePath := metadataFiles[key]
2019-11-21 12:12:30 +02:00
metadataFile, err := stepHelperData.OpenFile(configFilePath)
2019-11-08 16:35:11 +02:00
checkError(err)
defer metadataFile.Close()
fmt.Printf("Reading file %v\n", configFilePath)
err = stepData.ReadPipelineStepData(metadataFile)
checkError(err)
stepName := stepData.Metadata.Name
fmt.Printf("Step name: %v\n", stepName)
allSteps.Steps = append(allSteps.Steps, stepName)
2019-11-08 16:35:11 +02:00
osImport := false
osImport, err = setDefaultParameters(&stepData)
checkError(err)
2019-11-08 16:35:11 +02:00
myStepInfo, err := getStepInfo(&stepData, osImport, stepHelperData.ExportPrefix)
checkError(err)
2019-11-08 16:35:11 +02:00
step := stepTemplate(myStepInfo, "step", stepGoTemplate)
err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v_generated.go", stepName)), step, 0644)
checkError(err)
2019-11-08 16:35:11 +02:00
test := stepTemplate(myStepInfo, "stepTest", stepTestGoTemplate)
err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v_generated_test.go", stepName)), test, 0644)
checkError(err)
exists, _ := piperutils.FileExists(filepath.Join(targetDir, fmt.Sprintf("%v.go", stepName)))
if !exists {
impl := stepImplementation(myStepInfo, "impl", stepGoImplementationTemplate)
err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v.go", stepName)), impl, 0644)
checkError(err)
}
exists, _ = piperutils.FileExists(filepath.Join(targetDir, fmt.Sprintf("%v_test.go", stepName)))
if !exists {
impl := stepImplementation(myStepInfo, "implTest", stepGoImplementationTestTemplate)
err = stepHelperData.WriteFile(filepath.Join(targetDir, fmt.Sprintf("%v_test.go", stepName)), impl, 0644)
checkError(err)
}
2019-11-08 16:35:11 +02:00
}
// expose metadata functions
code := generateCode(allSteps, "metadata", metadataGeneratedTemplate, nil)
err := stepHelperData.WriteFile(filepath.Join(targetDir, metadataGeneratedFileName), code, 0644)
checkError(err)
2019-11-08 16:35:11 +02:00
return nil
}
func openMetaFile(name string) (io.ReadCloser, error) {
return os.Open(name)
}
func fileWriter(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm)
}
func setDefaultParameters(stepData *config.StepData) (bool, error) {
//ToDo: custom function for default handling, support all relevant parameter types
osImportRequired := false
for k, param := range stepData.Spec.Inputs.Parameters {
if param.Default == nil {
switch param.Type {
case "bool":
// ToDo: Check if default should be read from env
param.Default = "false"
case "int":
param.Default = "0"
case "string":
param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name)
osImportRequired = true
2019-11-08 16:35:11 +02:00
case "[]string":
// ToDo: Check if default should be read from env
param.Default = "[]string{}"
generator helper: support for map[string]interface{} (#2232) Up to now the code generator is not able to handle the type map[string]interface{} which is important for nested configurations. With that change we support such nested configuration. Fo now parameters with a map type are not supported via command line parameters. Those parameters are simply ommitted. But with this change is it possible to read such nested structures from the pipeline configuration (.pipeline/config.yml). As a next step we can discuss if we would like to support such values also via command line parameters. One possible approach could be ``` ./piper <command> -myParam key1=val1 --myParam key2=val2 ``` which gets finally collected inside our map: ``` map["key1"] = "val1" map["key2"] = "val2" ``` This is of course hard to do for deeper nestings. In that case providing a pointer to a file might be more suitable. In that context we need to consider how to - declare the default values for map like parameters in our metadata files. - deal with the different types we have for the parameter itself wrt the yaml like config on the one hand and on the level of the command line parameters on the other hand. Maybe for that we have to extend the metadata format (e.g. describe an alternate type receiving the values from the command line, like []string. With that approach values for simple nested (... not deep nested) params can be provided like described above, it would be possible to represent these parameters for the command line parser as string slice entries like "[]string{key1=val1, key2=val2". These parameters needs in this case transformed "by us" into the map we use further down the road. In case we agree in principle on an approach as outlined here we should adjust the golden files reflecting this use case.
2020-11-23 08:55:20 +02:00
case "map[string]interface{}":
// Currently we don't need to set a default here since in this case the default
// is never used. Needs to be changed in case we enable cli parameter handling
// for that type.
2019-11-08 16:35:11 +02:00
default:
return false, fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
}
} else {
switch param.Type {
case "bool":
boolVal := "false"
if param.Default.(bool) == true {
boolVal = "true"
}
param.Default = boolVal
case "int":
param.Default = fmt.Sprintf("%v", param.Default)
case "string":
param.Default = fmt.Sprintf("`%v`", param.Default)
2019-11-08 16:35:11 +02:00
case "[]string":
param.Default = fmt.Sprintf("[]string{`%v`}", strings.Join(getStringSliceFromInterface(param.Default), "`, `"))
generator helper: support for map[string]interface{} (#2232) Up to now the code generator is not able to handle the type map[string]interface{} which is important for nested configurations. With that change we support such nested configuration. Fo now parameters with a map type are not supported via command line parameters. Those parameters are simply ommitted. But with this change is it possible to read such nested structures from the pipeline configuration (.pipeline/config.yml). As a next step we can discuss if we would like to support such values also via command line parameters. One possible approach could be ``` ./piper <command> -myParam key1=val1 --myParam key2=val2 ``` which gets finally collected inside our map: ``` map["key1"] = "val1" map["key2"] = "val2" ``` This is of course hard to do for deeper nestings. In that case providing a pointer to a file might be more suitable. In that context we need to consider how to - declare the default values for map like parameters in our metadata files. - deal with the different types we have for the parameter itself wrt the yaml like config on the one hand and on the level of the command line parameters on the other hand. Maybe for that we have to extend the metadata format (e.g. describe an alternate type receiving the values from the command line, like []string. With that approach values for simple nested (... not deep nested) params can be provided like described above, it would be possible to represent these parameters for the command line parser as string slice entries like "[]string{key1=val1, key2=val2". These parameters needs in this case transformed "by us" into the map we use further down the road. In case we agree in principle on an approach as outlined here we should adjust the golden files reflecting this use case.
2020-11-23 08:55:20 +02:00
case "map[string]interface{}":
// Currently we don't need to set a default here since in this case the default
// is never used. Needs to be changed in case we enable cli parameter handling
// for that type.
2019-11-08 16:35:11 +02:00
default:
return false, fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
}
}
stepData.Spec.Inputs.Parameters[k] = param
}
return osImportRequired, nil
}
func getStepInfo(stepData *config.StepData, osImport bool, exportPrefix string) (stepInfo, error) {
oRes, err := getOutputResourceDetails(stepData)
2019-11-08 16:35:11 +02:00
return stepInfo{
StepName: stepData.Metadata.Name,
CobraCmdFuncName: fmt.Sprintf("%vCommand", strings.Title(stepData.Metadata.Name)),
CreateCmdVar: fmt.Sprintf("create%vCmd", strings.Title(stepData.Metadata.Name)),
Short: stepData.Metadata.Description,
Long: stepData.Metadata.LongDescription,
StepParameters: stepData.Spec.Inputs.Parameters,
StepAliases: stepData.Metadata.Aliases,
FlagsFunc: fmt.Sprintf("add%vFlags", strings.Title(stepData.Metadata.Name)),
OSImport: osImport,
OutputResources: oRes,
ExportPrefix: exportPrefix,
2020-04-16 14:37:45 +02:00
StepSecrets: getSecretFields(stepData),
Containers: stepData.Spec.Containers,
Sidecars: stepData.Spec.Sidecars,
Outputs: stepData.Spec.Outputs,
Resources: stepData.Spec.Inputs.Resources,
Secrets: stepData.Spec.Inputs.Secrets,
},
err
}
2020-04-16 14:37:45 +02:00
func getSecretFields(stepData *config.StepData) []string {
var secretFields []string
for _, parameter := range stepData.Spec.Inputs.Parameters {
if parameter.Secret {
secretFields = append(secretFields, parameter.Name)
}
}
return secretFields
}
func getOutputResourceDetails(stepData *config.StepData) ([]map[string]string, error) {
outputResources := []map[string]string{}
for _, res := range stepData.Spec.Outputs.Resources {
currentResource := map[string]string{}
currentResource["name"] = res.Name
switch res.Type {
case "piperEnvironment":
var envResource PiperEnvironmentResource
envResource.Name = res.Name
envResource.StepName = stepData.Metadata.Name
for _, param := range res.Parameters {
paramSections := strings.Split(fmt.Sprintf("%v", param["name"]), "/")
category := ""
name := paramSections[0]
if len(paramSections) > 1 {
name = strings.Join(paramSections[1:], "_")
category = paramSections[0]
if !contains(envResource.Categories, category) {
envResource.Categories = append(envResource.Categories, category)
}
}
envParam := PiperEnvironmentParameter{Category: category, Name: name, Type: fmt.Sprint(param["type"])}
envResource.Parameters = append(envResource.Parameters, envParam)
}
def, err := envResource.StructString()
if err != nil {
return outputResources, err
}
currentResource["def"] = def
currentResource["objectname"] = envResource.StructName()
outputResources = append(outputResources, currentResource)
case "influx":
var influxResource InfluxResource
influxResource.Name = res.Name
influxResource.StepName = stepData.Metadata.Name
for _, measurement := range res.Parameters {
influxMeasurement := InfluxMeasurement{Name: fmt.Sprintf("%v", measurement["name"])}
if fields, ok := measurement["fields"].([]interface{}); ok {
for _, field := range fields {
if fieldParams, ok := field.(map[string]interface{}); ok {
influxMeasurement.Fields = append(influxMeasurement.Fields, InfluxMetric{Name: fmt.Sprintf("%v", fieldParams["name"]), Type: fmt.Sprintf("%v", fieldParams["type"])})
}
}
}
if tags, ok := measurement["tags"].([]interface{}); ok {
for _, tag := range tags {
if tagParams, ok := tag.(map[string]interface{}); ok {
influxMeasurement.Tags = append(influxMeasurement.Tags, InfluxMetric{Name: fmt.Sprintf("%v", tagParams["name"])})
}
}
}
influxResource.Measurements = append(influxResource.Measurements, influxMeasurement)
}
def, err := influxResource.StructString()
if err != nil {
return outputResources, err
}
currentResource["def"] = def
currentResource["objectname"] = influxResource.StructName()
outputResources = append(outputResources, currentResource)
}
2019-11-08 16:35:11 +02:00
}
return outputResources, nil
2019-11-08 16:35:11 +02:00
}
// MetadataFiles provides a list of all step metadata files
func MetadataFiles(sourceDirectory string) ([]string, error) {
var metadataFiles []string
err := filepath.Walk(sourceDirectory, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) == ".yaml" {
metadataFiles = append(metadataFiles, path)
}
return nil
})
if err != nil {
return metadataFiles, nil
}
return metadataFiles, nil
}
generator helper: support for map[string]interface{} (#2232) Up to now the code generator is not able to handle the type map[string]interface{} which is important for nested configurations. With that change we support such nested configuration. Fo now parameters with a map type are not supported via command line parameters. Those parameters are simply ommitted. But with this change is it possible to read such nested structures from the pipeline configuration (.pipeline/config.yml). As a next step we can discuss if we would like to support such values also via command line parameters. One possible approach could be ``` ./piper <command> -myParam key1=val1 --myParam key2=val2 ``` which gets finally collected inside our map: ``` map["key1"] = "val1" map["key2"] = "val2" ``` This is of course hard to do for deeper nestings. In that case providing a pointer to a file might be more suitable. In that context we need to consider how to - declare the default values for map like parameters in our metadata files. - deal with the different types we have for the parameter itself wrt the yaml like config on the one hand and on the level of the command line parameters on the other hand. Maybe for that we have to extend the metadata format (e.g. describe an alternate type receiving the values from the command line, like []string. With that approach values for simple nested (... not deep nested) params can be provided like described above, it would be possible to represent these parameters for the command line parser as string slice entries like "[]string{key1=val1, key2=val2". These parameters needs in this case transformed "by us" into the map we use further down the road. In case we agree in principle on an approach as outlined here we should adjust the golden files reflecting this use case.
2020-11-23 08:55:20 +02:00
func isCLIParam(myType string) bool {
return myType != "map[string]interface{}"
}
func stepTemplate(myStepInfo stepInfo, templateName, goTemplate string) []byte {
funcMap := sprig.HermeticTxtFuncMap()
funcMap["flagType"] = flagType
funcMap["golangName"] = golangNameTitle
funcMap["title"] = strings.Title
funcMap["longName"] = longName
funcMap["uniqueName"] = mustUniqName
generator helper: support for map[string]interface{} (#2232) Up to now the code generator is not able to handle the type map[string]interface{} which is important for nested configurations. With that change we support such nested configuration. Fo now parameters with a map type are not supported via command line parameters. Those parameters are simply ommitted. But with this change is it possible to read such nested structures from the pipeline configuration (.pipeline/config.yml). As a next step we can discuss if we would like to support such values also via command line parameters. One possible approach could be ``` ./piper <command> -myParam key1=val1 --myParam key2=val2 ``` which gets finally collected inside our map: ``` map["key1"] = "val1" map["key2"] = "val2" ``` This is of course hard to do for deeper nestings. In that case providing a pointer to a file might be more suitable. In that context we need to consider how to - declare the default values for map like parameters in our metadata files. - deal with the different types we have for the parameter itself wrt the yaml like config on the one hand and on the level of the command line parameters on the other hand. Maybe for that we have to extend the metadata format (e.g. describe an alternate type receiving the values from the command line, like []string. With that approach values for simple nested (... not deep nested) params can be provided like described above, it would be possible to represent these parameters for the command line parser as string slice entries like "[]string{key1=val1, key2=val2". These parameters needs in this case transformed "by us" into the map we use further down the road. In case we agree in principle on an approach as outlined here we should adjust the golden files reflecting this use case.
2020-11-23 08:55:20 +02:00
funcMap["isCLIParam"] = isCLIParam
2019-11-08 16:35:11 +02:00
return generateCode(myStepInfo, templateName, goTemplate, funcMap)
2019-11-08 16:35:11 +02:00
}
func stepImplementation(myStepInfo stepInfo, templateName, goTemplate string) []byte {
funcMap := sprig.HermeticTxtFuncMap()
funcMap["title"] = strings.Title
funcMap["uniqueName"] = mustUniqName
2019-11-08 16:35:11 +02:00
return generateCode(myStepInfo, templateName, goTemplate, funcMap)
2019-11-08 16:35:11 +02:00
}
func generateCode(dataObject interface{}, templateName, goTemplate string, funcMap template.FuncMap) []byte {
tmpl, err := template.New(templateName).Funcs(funcMap).Parse(goTemplate)
checkError(err)
var generatedCode bytes.Buffer
err = tmpl.Execute(&generatedCode, dataObject)
checkError(err)
return generatedCode.Bytes()
}
2019-11-08 16:35:11 +02:00
func longName(long string) string {
l := strings.ReplaceAll(long, "`", "` + \"`\" + `")
l = strings.TrimSpace(l)
return l
}
func resourceFieldType(fieldType string) string {
//TODO: clarify why fields are initialized with <nil> and tags are initialized with ''
if len(fieldType) == 0 || fieldType == "<nil>" {
return "string"
}
return fieldType
}
2019-11-08 16:35:11 +02:00
func golangName(name string) string {
properName := strings.Replace(name, "Api", "API", -1)
properName = strings.Replace(properName, "api", "API", -1)
properName = strings.Replace(properName, "Url", "URL", -1)
properName = strings.Replace(properName, "Id", "ID", -1)
properName = strings.Replace(properName, "Json", "JSON", -1)
properName = strings.Replace(properName, "json", "JSON", -1)
properName = strings.Replace(properName, "Tls", "TLS", -1)
return properName
}
func golangNameTitle(name string) string {
return strings.Title(golangName(name))
2019-11-08 16:35:11 +02:00
}
func flagType(paramType string) string {
var theFlagType string
switch paramType {
case "bool":
theFlagType = "BoolVar"
case "int":
theFlagType = "IntVar"
2019-11-08 16:35:11 +02:00
case "string":
theFlagType = "StringVar"
case "[]string":
theFlagType = "StringSliceVar"
default:
fmt.Printf("Meta data type not set or not known: '%v'\n", paramType)
os.Exit(1)
}
return theFlagType
}
func getStringSliceFromInterface(iSlice interface{}) []string {
s := []string{}
t, ok := iSlice.([]interface{})
if ok {
for _, v := range t {
s = append(s, fmt.Sprintf("%v", v))
}
} else {
s = append(s, fmt.Sprintf("%v", iSlice))
}
return s
}
func mustUniqName(list []config.StepParameters) ([]config.StepParameters, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
names := []string{}
dest := []config.StepParameters{}
var item config.StepParameters
for i := 0; i < l; i++ {
item = l2.Index(i).Interface().(config.StepParameters)
if !piperutils.ContainsString(names, item.Name) {
names = append(names, item.Name)
dest = append(dest, item)
}
}
return dest, nil
default:
return nil, fmt.Errorf("Cannot find uniq on type %s", tp)
}
}