package config import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperenv" "github.com/ghodss/yaml" "github.com/pkg/errors" ) // StepData defines the metadata for a step, like step descriptions, parameters, ... type StepData struct { Metadata StepMetadata `json:"metadata"` Spec StepSpec `json:"spec"` } // StepMetadata defines the metadata for a step, like step descriptions, parameters, ... type StepMetadata struct { Name string `json:"name"` Aliases []Alias `json:"aliases,omitempty"` Description string `json:"description"` LongDescription string `json:"longDescription,omitempty"` } // StepSpec defines the spec details for a step, like step inputs, containers, sidecars, ... type StepSpec struct { Inputs StepInputs `json:"inputs,omitempty"` Outputs StepOutputs `json:"outputs,omitempty"` Containers []Container `json:"containers,omitempty"` Sidecars []Container `json:"sidecars,omitempty"` } // StepInputs defines the spec details for a step, like step inputs, containers, sidecars, ... type StepInputs struct { Parameters []StepParameters `json:"params"` Resources []StepResources `json:"resources,omitempty"` Secrets []StepSecrets `json:"secrets,omitempty"` } // StepParameters defines the parameters for a step type StepParameters struct { Name string `json:"name"` Description string `json:"description"` LongDescription string `json:"longDescription,omitempty"` ResourceRef []ResourceReference `json:"resourceRef,omitempty"` Scope []string `json:"scope"` Type string `json:"type"` Mandatory bool `json:"mandatory,omitempty"` Default interface{} `json:"default,omitempty"` PossibleValues []interface{} `json:"possibleValues,omitempty"` Aliases []Alias `json:"aliases,omitempty"` Conditions []Condition `json:"conditions,omitempty"` Secret bool `json:"secret,omitempty"` MandatoryIf []ParameterDependence `json:"mandatoryIf,omitempty"` } type ParameterDependence struct { Name string `json:"name"` Value string `json:"value"` } // ResourceReference defines the parameters of a resource reference type ResourceReference struct { Name string `json:"name"` Type string `json:"type,omitempty"` Param string `json:"param,omitempty"` Default string `json:"default,omitempty"` Aliases []Alias `json:"aliases,omitempty"` } // Alias defines a step input parameter alias type Alias struct { Name string `json:"name,omitempty"` Deprecated bool `json:"deprecated,omitempty"` } // StepResources defines the resources to be provided by the step context, e.g. Jenkins pipeline type StepResources struct { Name string `json:"name"` Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` Parameters []map[string]interface{} `json:"params,omitempty"` Conditions []Condition `json:"conditions,omitempty"` } // StepSecrets defines the secrets to be provided by the step context, e.g. Jenkins pipeline type StepSecrets struct { Name string `json:"name"` Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` Aliases []Alias `json:"aliases,omitempty"` } // StepOutputs defines the outputs of a step step, typically one or multiple resources type StepOutputs struct { Resources []StepResources `json:"resources,omitempty"` } // Container defines an execution container type Container struct { //ToDo: check dockerOptions, dockerVolumeBind, containerPortMappings, sidecarOptions, sidecarVolumeBind Command []string `json:"command"` EnvVars []EnvVar `json:"env"` Image string `json:"image"` ImagePullPolicy string `json:"imagePullPolicy"` Name string `json:"name"` ReadyCommand string `json:"readyCommand"` Shell string `json:"shell"` WorkingDir string `json:"workingDir"` Conditions []Condition `json:"conditions,omitempty"` Options []Option `json:"options,omitempty"` //VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` } // ToDo: Add the missing Volumes part to enable the volume mount completely // VolumeMount defines a mount path // type VolumeMount struct { // MountPath string `json:"mountPath"` // Name string `json:"name"` //} // Option defines an docker option type Option struct { Name string `json:"name"` Value string `json:"value"` } // EnvVar defines an environment variable type EnvVar struct { Name string `json:"name"` Value string `json:"value"` } // Condition defines an condition which decides when the parameter, resource or container is valid type Condition struct { ConditionRef string `json:"conditionRef"` Params []Param `json:"params"` } // Param defines the parameters serving as inputs to the condition type Param struct { Name string `json:"name"` Value string `json:"value"` } // StepFilters defines the filter parameters for the different sections type StepFilters struct { All []string General []string Stages []string Steps []string Parameters []string Env []string } // ReadPipelineStepData loads step definition in yaml format func (m *StepData) ReadPipelineStepData(metadata io.ReadCloser) error { defer metadata.Close() content, err := ioutil.ReadAll(metadata) if err != nil { return errors.Wrapf(err, "error reading %v", metadata) } err = yaml.Unmarshal(content, &m) if err != nil { return errors.Wrapf(err, "error unmarshalling: %v", err) } return nil } // GetParameterFilters retrieves all scope dependent parameter filters func (m *StepData) GetParameterFilters() StepFilters { filters := StepFilters{All: []string{"verbose"}, General: []string{"verbose"}, Steps: []string{"verbose"}, Stages: []string{"verbose"}, Parameters: []string{"verbose"}} for _, param := range m.Spec.Inputs.Parameters { parameterKeys := []string{param.Name} for _, condition := range param.Conditions { for _, dependentParam := range condition.Params { parameterKeys = append(parameterKeys, dependentParam.Value) } } filters.All = append(filters.All, parameterKeys...) for _, scope := range param.Scope { switch scope { case "GENERAL": filters.General = append(filters.General, parameterKeys...) case "STEPS": filters.Steps = append(filters.Steps, parameterKeys...) case "STAGES": filters.Stages = append(filters.Stages, parameterKeys...) case "PARAMETERS": filters.Parameters = append(filters.Parameters, parameterKeys...) case "ENV": filters.Env = append(filters.Env, parameterKeys...) } } } return filters } // GetContextParameterFilters retrieves all scope dependent parameter filters func (m *StepData) GetContextParameterFilters() StepFilters { var filters StepFilters contextFilters := []string{} for _, secret := range m.Spec.Inputs.Secrets { contextFilters = append(contextFilters, secret.Name) } if len(m.Spec.Inputs.Resources) > 0 { for _, res := range m.Spec.Inputs.Resources { if res.Type == "stash" { contextFilters = append(contextFilters, "stashContent") break } } } if len(m.Spec.Containers) > 0 { parameterKeys := []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerName", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"} for _, container := range m.Spec.Containers { for _, condition := range container.Conditions { for _, dependentParam := range condition.Params { parameterKeys = append(parameterKeys, dependentParam.Value) parameterKeys = append(parameterKeys, dependentParam.Name) } } } // ToDo: append dependentParam.Value & dependentParam.Name only according to correct parameter scope and not generally contextFilters = append(contextFilters, parameterKeys...) } if len(m.Spec.Sidecars) > 0 { //ToDo: support fallback for "dockerName" configuration property -> via aliasing? contextFilters = append(contextFilters, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}...) //ToDo: add condition param.Value and param.Name to filter as for Containers } contextFilters = addVaultContextParametersFilter(m, contextFilters) if len(contextFilters) > 0 { filters.All = append(filters.All, contextFilters...) filters.General = append(filters.General, contextFilters...) filters.Steps = append(filters.Steps, contextFilters...) filters.Stages = append(filters.Stages, contextFilters...) filters.Parameters = append(filters.Parameters, contextFilters...) filters.Env = append(filters.Env, contextFilters...) } return filters } func addVaultContextParametersFilter(m *StepData, contextFilters []string) []string { contextFilters = append(contextFilters, []string{"vaultAppRoleTokenCredentialsId", "vaultAppRoleSecretTokenCredentialsId", "vaultTokenCredentialsId"}...) return contextFilters } // GetContextDefaults retrieves context defaults like container image, name, env vars, resources, ... // It only supports scenarios with one container and optionally one sidecar func (m *StepData) GetContextDefaults(stepName string) (io.ReadCloser, error) { //ToDo error handling empty Containers/Sidecars //ToDo handle empty Command root := map[string]interface{}{} if len(m.Spec.Containers) > 0 { for _, container := range m.Spec.Containers { key := "" conditionParam := "" if len(container.Conditions) > 0 { key = container.Conditions[0].Params[0].Value conditionParam = container.Conditions[0].Params[0].Name } p := map[string]interface{}{} if key != "" { root[key] = p //add default for condition parameter if available for _, inputParam := range m.Spec.Inputs.Parameters { if inputParam.Name == conditionParam { root[conditionParam] = inputParam.Default } } } else { p = root } if len(container.Command) > 0 { p["containerCommand"] = container.Command[0] } putStringIfNotEmpty(p, "containerName", container.Name) putStringIfNotEmpty(p, "containerShell", container.Shell) container.commonConfiguration("docker", &p) // Ready command not relevant for main runtime container so far //putStringIfNotEmpty(p, ..., container.ReadyCommand) } } if len(m.Spec.Sidecars) > 0 { if len(m.Spec.Sidecars[0].Command) > 0 { root["sidecarCommand"] = m.Spec.Sidecars[0].Command[0] } m.Spec.Sidecars[0].commonConfiguration("sidecar", &root) putStringIfNotEmpty(root, "sidecarReadyCommand", m.Spec.Sidecars[0].ReadyCommand) // not filled for now since this is not relevant in Kubernetes case //putStringIfNotEmpty(root, "containerPortMappings", m.Spec.Sidecars[0].) } if len(m.Spec.Inputs.Resources) > 0 { keys := []string{} resources := map[string][]string{} for _, resource := range m.Spec.Inputs.Resources { if resource.Type == "stash" { key := "" if len(resource.Conditions) > 0 { key = resource.Conditions[0].Params[0].Value } if resources[key] == nil { keys = append(keys, key) resources[key] = []string{} } resources[key] = append(resources[key], resource.Name) } } for _, key := range keys { if key == "" { root["stashContent"] = resources[""] } else { if root[key] == nil { root[key] = map[string]interface{}{ "stashContent": resources[key], } } else { p := root[key].(map[string]interface{}) p["stashContent"] = resources[key] } } } } c := Config{ Steps: map[string]map[string]interface{}{ stepName: root, }, } JSON, err := yaml.Marshal(c) if err != nil { return nil, errors.Wrap(err, "failed to create context defaults") } r := ioutil.NopCloser(bytes.NewReader(JSON)) return r, nil } // GetResourceParameters retrieves parameters from a named pipeline resource with a defined path func (m *StepData) GetResourceParameters(path, name string) map[string]interface{} { resourceParams := map[string]interface{}{} for _, param := range m.Spec.Inputs.Parameters { for _, res := range param.ResourceRef { if res.Name == name { if val := getParameterValue(path, res, param); val != nil { resourceParams[param.Name] = val } } } } return resourceParams } func (container *Container) commonConfiguration(keyPrefix string, config *map[string]interface{}) { putMapIfNotEmpty(*config, keyPrefix+"EnvVars", EnvVarsAsMap(container.EnvVars)) putStringIfNotEmpty(*config, keyPrefix+"Image", container.Image) putStringIfNotEmpty(*config, keyPrefix+"Name", container.Name) if container.ImagePullPolicy != "" { (*config)[keyPrefix+"PullImage"] = container.ImagePullPolicy != "Never" } putStringIfNotEmpty(*config, keyPrefix+"Workspace", container.WorkingDir) putSliceIfNotEmpty(*config, keyPrefix+"Options", OptionsAsStringSlice(container.Options)) //putSliceIfNotEmpty(*config, keyPrefix+"VolumeBind", volumeMountsAsStringSlice(container.VolumeMounts)) } func getParameterValue(path string, res ResourceReference, param StepParameters) interface{} { paramName := res.Param if param.Type != "string" { paramName += ".json" } if val := piperenv.GetResourceParameter(path, res.Name, paramName); len(val) > 0 { if param.Type != "string" { var unmarshalledValue interface{} err := json.Unmarshal([]byte(val), &unmarshalledValue) if err != nil { log.Entry().Debugf("Failed to unmarshal: %v", val) } return unmarshalledValue } return val } return nil } // GetReference returns the ResourceReference of the given type func (m *StepParameters) GetReference(refType string) *ResourceReference { for _, ref := range m.ResourceRef { if refType == ref.Type { return &ref } } return nil } func getFilterForResourceReferences(params []StepParameters) []string { var filter []string for _, param := range params { reference := param.GetReference("vaultSecret") if reference == nil { reference = param.GetReference("vaultSecretFile") } if reference == nil { return filter } if reference.Name != "" { filter = append(filter, reference.Name) } } return filter } // HasReference checks whether StepData contains a parameter that has Reference with the given type func (m *StepData) HasReference(refType string) bool { for _, param := range m.Spec.Inputs.Parameters { if param.GetReference(refType) != nil { return true } } return false } // EnvVarsAsMap converts container EnvVars into a map as required by dockerExecute func EnvVarsAsMap(envVars []EnvVar) map[string]string { e := map[string]string{} for _, v := range envVars { e[v.Name] = v.Value } return e } // OptionsAsStringSlice converts container options into a string slice as required by dockerExecute func OptionsAsStringSlice(options []Option) []string { e := []string{} for _, v := range options { if len(v.Value) != 0 { e = append(e, fmt.Sprintf("%v %v", v.Name, v.Value)) } else { e = append(e, fmt.Sprintf("%v=", v.Name)) } } return e } func putStringIfNotEmpty(config map[string]interface{}, key, value string) { if value != "" { config[key] = value } } func putMapIfNotEmpty(config map[string]interface{}, key string, value map[string]string) { if len(value) > 0 { config[key] = value } } func putSliceIfNotEmpty(config map[string]interface{}, key string, value []string) { if len(value) > 0 { config[key] = value } } func ResolveMetadata(gitHubTokens map[string]string, metaDataResolver func() map[string]StepData, stepMetadata string, stepName string) (StepData, error) { var metadata StepData if stepMetadata != "" { metadataFile, err := OpenPiperFile(stepMetadata, gitHubTokens) if err != nil { return metadata, errors.Wrap(err, "open failed") } err = metadata.ReadPipelineStepData(metadataFile) if err != nil { return metadata, errors.Wrap(err, "read failed") } } else { if stepName != "" { if metaDataResolver == nil { return metadata, errors.New("metaDataResolver is nil") } metadataMap := metaDataResolver() var ok bool metadata, ok = metadataMap[stepName] if !ok { return metadata, errors.Errorf("could not retrieve by stepName %v", stepName) } } else { return metadata, errors.Errorf("either one of stepMetadata or stepName parameter has to be passed") } } return metadata, nil } //ToDo: Enable this when the Volumes part is also implemented //func volumeMountsAsStringSlice(volumeMounts []VolumeMount) []string { // e := []string{} // for _, v := range volumeMounts { // e = append(e, fmt.Sprintf("%v:%v", v.Name, v.MountPath)) // } // return e //}