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

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  
     9  	"github.com/SAP/jenkins-library/pkg/log"
    10  	"github.com/SAP/jenkins-library/pkg/piperenv"
    11  
    12  	"github.com/ghodss/yaml"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  // StepData defines the metadata for a step, like step descriptions, parameters, ...
    17  type StepData struct {
    18  	Metadata StepMetadata `json:"metadata"`
    19  	Spec     StepSpec     `json:"spec"`
    20  }
    21  
    22  // StepMetadata defines the metadata for a step, like step descriptions, parameters, ...
    23  type StepMetadata struct {
    24  	Name            string  `json:"name"`
    25  	Aliases         []Alias `json:"aliases,omitempty"`
    26  	Description     string  `json:"description"`
    27  	LongDescription string  `json:"longDescription,omitempty"`
    28  }
    29  
    30  // StepSpec defines the spec details for a step, like step inputs, containers, sidecars, ...
    31  type StepSpec struct {
    32  	Inputs     StepInputs  `json:"inputs,omitempty"`
    33  	Outputs    StepOutputs `json:"outputs,omitempty"`
    34  	Containers []Container `json:"containers,omitempty"`
    35  	Sidecars   []Container `json:"sidecars,omitempty"`
    36  }
    37  
    38  // StepInputs defines the spec details for a step, like step inputs, containers, sidecars, ...
    39  type StepInputs struct {
    40  	Parameters []StepParameters `json:"params"`
    41  	Resources  []StepResources  `json:"resources,omitempty"`
    42  	Secrets    []StepSecrets    `json:"secrets,omitempty"`
    43  }
    44  
    45  // StepParameters defines the parameters for a step
    46  type StepParameters struct {
    47  	Name               string                `json:"name"`
    48  	Description        string                `json:"description"`
    49  	LongDescription    string                `json:"longDescription,omitempty"`
    50  	ResourceRef        []ResourceReference   `json:"resourceRef,omitempty"`
    51  	Scope              []string              `json:"scope"`
    52  	Type               string                `json:"type"`
    53  	Mandatory          bool                  `json:"mandatory,omitempty"`
    54  	Default            interface{}           `json:"default,omitempty"`
    55  	PossibleValues     []interface{}         `json:"possibleValues,omitempty"`
    56  	Aliases            []Alias               `json:"aliases,omitempty"`
    57  	Conditions         []Condition           `json:"conditions,omitempty"`
    58  	Secret             bool                  `json:"secret,omitempty"`
    59  	MandatoryIf        []ParameterDependence `json:"mandatoryIf,omitempty"`
    60  	DeprecationMessage string                `json:"deprecationMessage,omitempty"`
    61  }
    62  
    63  type ParameterDependence struct {
    64  	Name  string `json:"name"`
    65  	Value string `json:"value"`
    66  }
    67  
    68  // ResourceReference defines the parameters of a resource reference
    69  type ResourceReference struct {
    70  	Name    string  `json:"name"`
    71  	Type    string  `json:"type,omitempty"`
    72  	Param   string  `json:"param,omitempty"`
    73  	Default string  `json:"default,omitempty"`
    74  	Aliases []Alias `json:"aliases,omitempty"`
    75  }
    76  
    77  // Alias defines a step input parameter alias
    78  type Alias struct {
    79  	Name       string `json:"name,omitempty"`
    80  	Deprecated bool   `json:"deprecated,omitempty"`
    81  }
    82  
    83  // StepResources defines the resources to be provided by the step context, e.g. Jenkins pipeline
    84  type StepResources struct {
    85  	Name        string                   `json:"name"`
    86  	Description string                   `json:"description,omitempty"`
    87  	Type        string                   `json:"type,omitempty"`
    88  	Parameters  []map[string]interface{} `json:"params,omitempty"`
    89  	Conditions  []Condition              `json:"conditions,omitempty"`
    90  }
    91  
    92  // StepSecrets defines the secrets to be provided by the step context, e.g. Jenkins pipeline
    93  type StepSecrets struct {
    94  	Name        string  `json:"name"`
    95  	Description string  `json:"description,omitempty"`
    96  	Type        string  `json:"type,omitempty"`
    97  	Aliases     []Alias `json:"aliases,omitempty"`
    98  }
    99  
   100  // StepOutputs defines the outputs of a step step, typically one or multiple resources
   101  type StepOutputs struct {
   102  	Resources []StepResources `json:"resources,omitempty"`
   103  }
   104  
   105  // Container defines an execution container
   106  type Container struct {
   107  	//ToDo: check dockerOptions, dockerVolumeBind, containerPortMappings, sidecarOptions, sidecarVolumeBind
   108  	Command         []string    `json:"command"`
   109  	EnvVars         []EnvVar    `json:"env"`
   110  	Image           string      `json:"image"`
   111  	ImagePullPolicy string      `json:"imagePullPolicy"`
   112  	Name            string      `json:"name"`
   113  	ReadyCommand    string      `json:"readyCommand"`
   114  	Shell           string      `json:"shell"`
   115  	WorkingDir      string      `json:"workingDir"`
   116  	Conditions      []Condition `json:"conditions,omitempty"`
   117  	Options         []Option    `json:"options,omitempty"`
   118  	//VolumeMounts    []VolumeMount `json:"volumeMounts,omitempty"`
   119  }
   120  
   121  // ToDo: Add the missing Volumes part to enable the volume mount completely
   122  // VolumeMount defines a mount path
   123  // type VolumeMount struct {
   124  //	MountPath string `json:"mountPath"`
   125  //	Name      string `json:"name"`
   126  //}
   127  
   128  // Option defines an docker option
   129  type Option struct {
   130  	Name  string `json:"name"`
   131  	Value string `json:"value"`
   132  }
   133  
   134  // EnvVar defines an environment variable
   135  type EnvVar struct {
   136  	Name  string `json:"name"`
   137  	Value string `json:"value"`
   138  }
   139  
   140  // Condition defines an condition which decides when the parameter, resource or container is valid
   141  type Condition struct {
   142  	ConditionRef string  `json:"conditionRef"`
   143  	Params       []Param `json:"params"`
   144  }
   145  
   146  // Param defines the parameters serving as inputs to the condition
   147  type Param struct {
   148  	Name  string `json:"name"`
   149  	Value string `json:"value"`
   150  }
   151  
   152  // StepFilters defines the filter parameters for the different sections
   153  type StepFilters struct {
   154  	All        []string
   155  	General    []string
   156  	Stages     []string
   157  	Steps      []string
   158  	Parameters []string
   159  	Env        []string
   160  }
   161  
   162  // ReadPipelineStepData loads step definition in yaml format
   163  func (m *StepData) ReadPipelineStepData(metadata io.ReadCloser) error {
   164  	defer metadata.Close()
   165  	content, err := io.ReadAll(metadata)
   166  	if err != nil {
   167  		return errors.Wrapf(err, "error reading %v", metadata)
   168  	}
   169  
   170  	err = yaml.Unmarshal(content, &m)
   171  	if err != nil {
   172  		return errors.Wrapf(err, "error unmarshalling: %v", err)
   173  	}
   174  	return nil
   175  }
   176  
   177  // GetParameterFilters retrieves all scope dependent parameter filters
   178  func (m *StepData) GetParameterFilters() StepFilters {
   179  	filters := StepFilters{All: []string{"verbose"}, General: []string{"verbose"}, Steps: []string{"verbose"}, Stages: []string{"verbose"}, Parameters: []string{"verbose"}}
   180  	for _, param := range m.Spec.Inputs.Parameters {
   181  		parameterKeys := []string{param.Name}
   182  		for _, condition := range param.Conditions {
   183  			for _, dependentParam := range condition.Params {
   184  				parameterKeys = append(parameterKeys, dependentParam.Value)
   185  			}
   186  		}
   187  		filters.All = append(filters.All, parameterKeys...)
   188  		for _, scope := range param.Scope {
   189  			switch scope {
   190  			case "GENERAL":
   191  				filters.General = append(filters.General, parameterKeys...)
   192  			case "STEPS":
   193  				filters.Steps = append(filters.Steps, parameterKeys...)
   194  			case "STAGES":
   195  				filters.Stages = append(filters.Stages, parameterKeys...)
   196  			case "PARAMETERS":
   197  				filters.Parameters = append(filters.Parameters, parameterKeys...)
   198  			case "ENV":
   199  				filters.Env = append(filters.Env, parameterKeys...)
   200  			}
   201  		}
   202  	}
   203  	return filters
   204  }
   205  
   206  // GetContextParameterFilters retrieves all scope dependent parameter filters
   207  func (m *StepData) GetContextParameterFilters() StepFilters {
   208  	var filters StepFilters
   209  	contextFilters := []string{}
   210  	for _, secret := range m.Spec.Inputs.Secrets {
   211  		contextFilters = append(contextFilters, secret.Name)
   212  	}
   213  
   214  	if len(m.Spec.Inputs.Resources) > 0 {
   215  		for _, res := range m.Spec.Inputs.Resources {
   216  			if res.Type == "stash" {
   217  				contextFilters = append(contextFilters, "stashContent")
   218  				break
   219  			}
   220  		}
   221  	}
   222  	if len(m.Spec.Containers) > 0 {
   223  		parameterKeys := []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerName", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "dockerRegistryUrl", "dockerRegistryCredentialsId"}
   224  		for _, container := range m.Spec.Containers {
   225  			for _, condition := range container.Conditions {
   226  				for _, dependentParam := range condition.Params {
   227  					parameterKeys = append(parameterKeys, dependentParam.Value)
   228  					parameterKeys = append(parameterKeys, dependentParam.Name)
   229  				}
   230  			}
   231  		}
   232  		// ToDo: append dependentParam.Value & dependentParam.Name only according to correct parameter scope and not generally
   233  		contextFilters = append(contextFilters, parameterKeys...)
   234  	}
   235  	if len(m.Spec.Sidecars) > 0 {
   236  		//ToDo: support fallback for "dockerName" configuration property -> via aliasing?
   237  		contextFilters = append(contextFilters, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}...)
   238  		//ToDo: add condition param.Value and param.Name to filter as for Containers
   239  	}
   240  
   241  	contextFilters = addVaultContextParametersFilter(m, contextFilters)
   242  
   243  	if len(contextFilters) > 0 {
   244  		filters.All = append(filters.All, contextFilters...)
   245  		filters.General = append(filters.General, contextFilters...)
   246  		filters.Steps = append(filters.Steps, contextFilters...)
   247  		filters.Stages = append(filters.Stages, contextFilters...)
   248  		filters.Parameters = append(filters.Parameters, contextFilters...)
   249  		filters.Env = append(filters.Env, contextFilters...)
   250  
   251  	}
   252  	return filters
   253  }
   254  
   255  func addVaultContextParametersFilter(m *StepData, contextFilters []string) []string {
   256  	contextFilters = append(contextFilters, []string{"vaultAppRoleTokenCredentialsId",
   257  		"vaultAppRoleSecretTokenCredentialsId", "vaultTokenCredentialsId"}...)
   258  	return contextFilters
   259  }
   260  
   261  // GetContextDefaults retrieves context defaults like container image, name, env vars, resources, ...
   262  // It only supports scenarios with one container and optionally one sidecar
   263  func (m *StepData) GetContextDefaults(stepName string) (io.ReadCloser, error) {
   264  
   265  	//ToDo error handling empty Containers/Sidecars
   266  	//ToDo handle empty Command
   267  	root := map[string]interface{}{}
   268  	if len(m.Spec.Containers) > 0 {
   269  		for _, container := range m.Spec.Containers {
   270  			key := ""
   271  			conditionParam := ""
   272  			if len(container.Conditions) > 0 {
   273  				key = container.Conditions[0].Params[0].Value
   274  				conditionParam = container.Conditions[0].Params[0].Name
   275  			}
   276  			p := map[string]interface{}{}
   277  			if key != "" {
   278  				root[key] = p
   279  				//add default for condition parameter if available
   280  				for _, inputParam := range m.Spec.Inputs.Parameters {
   281  					if inputParam.Name == conditionParam {
   282  						root[conditionParam] = inputParam.Default
   283  					}
   284  				}
   285  			} else {
   286  				p = root
   287  			}
   288  			if len(container.Command) > 0 {
   289  				p["containerCommand"] = container.Command[0]
   290  			}
   291  
   292  			putStringIfNotEmpty(p, "containerName", container.Name)
   293  			putStringIfNotEmpty(p, "containerShell", container.Shell)
   294  			container.commonConfiguration("docker", &p)
   295  
   296  			// Ready command not relevant for main runtime container so far
   297  			//putStringIfNotEmpty(p, ..., container.ReadyCommand)
   298  		}
   299  
   300  	}
   301  
   302  	if len(m.Spec.Sidecars) > 0 {
   303  		if len(m.Spec.Sidecars[0].Command) > 0 {
   304  			root["sidecarCommand"] = m.Spec.Sidecars[0].Command[0]
   305  		}
   306  		m.Spec.Sidecars[0].commonConfiguration("sidecar", &root)
   307  		putStringIfNotEmpty(root, "sidecarReadyCommand", m.Spec.Sidecars[0].ReadyCommand)
   308  
   309  		// not filled for now since this is not relevant in Kubernetes case
   310  		//putStringIfNotEmpty(root, "containerPortMappings", m.Spec.Sidecars[0].)
   311  	}
   312  
   313  	if len(m.Spec.Inputs.Resources) > 0 {
   314  		keys := []string{}
   315  		resources := map[string][]string{}
   316  		for _, resource := range m.Spec.Inputs.Resources {
   317  			if resource.Type == "stash" {
   318  				key := ""
   319  				if len(resource.Conditions) > 0 {
   320  					key = resource.Conditions[0].Params[0].Value
   321  				}
   322  				if resources[key] == nil {
   323  					keys = append(keys, key)
   324  					resources[key] = []string{}
   325  				}
   326  				resources[key] = append(resources[key], resource.Name)
   327  			}
   328  		}
   329  
   330  		for _, key := range keys {
   331  			if key == "" {
   332  				root["stashContent"] = resources[""]
   333  			} else {
   334  				if root[key] == nil {
   335  					root[key] = map[string]interface{}{
   336  						"stashContent": resources[key],
   337  					}
   338  				} else {
   339  					p := root[key].(map[string]interface{})
   340  					p["stashContent"] = resources[key]
   341  				}
   342  			}
   343  		}
   344  	}
   345  
   346  	c := Config{
   347  		Steps: map[string]map[string]interface{}{
   348  			stepName: root,
   349  		},
   350  	}
   351  
   352  	JSON, err := yaml.Marshal(c)
   353  	if err != nil {
   354  		return nil, errors.Wrap(err, "failed to create context defaults")
   355  	}
   356  
   357  	r := io.NopCloser(bytes.NewReader(JSON))
   358  	return r, nil
   359  }
   360  
   361  // GetResourceParameters retrieves parameters from a named pipeline resource with a defined path
   362  func (m *StepData) GetResourceParameters(path, name string) map[string]interface{} {
   363  	resourceParams := map[string]interface{}{}
   364  
   365  	for _, param := range m.Spec.Inputs.Parameters {
   366  		for _, res := range param.ResourceRef {
   367  			if res.Name == name {
   368  				if val := getParameterValue(path, res, param); val != nil {
   369  					resourceParams[param.Name] = val
   370  					break
   371  				}
   372  			}
   373  		}
   374  	}
   375  
   376  	return resourceParams
   377  }
   378  
   379  func (container *Container) commonConfiguration(keyPrefix string, config *map[string]interface{}) {
   380  	putMapIfNotEmpty(*config, keyPrefix+"EnvVars", EnvVarsAsMap(container.EnvVars))
   381  	putStringIfNotEmpty(*config, keyPrefix+"Image", container.Image)
   382  	putStringIfNotEmpty(*config, keyPrefix+"Name", container.Name)
   383  	if container.ImagePullPolicy != "" {
   384  		(*config)[keyPrefix+"PullImage"] = container.ImagePullPolicy != "Never"
   385  	}
   386  	putStringIfNotEmpty(*config, keyPrefix+"Workspace", container.WorkingDir)
   387  	putSliceIfNotEmpty(*config, keyPrefix+"Options", OptionsAsStringSlice(container.Options))
   388  	//putSliceIfNotEmpty(*config, keyPrefix+"VolumeBind", volumeMountsAsStringSlice(container.VolumeMounts))
   389  
   390  }
   391  
   392  func getParameterValue(path string, res ResourceReference, param StepParameters) interface{} {
   393  	paramName := res.Param
   394  	if param.Type != "string" {
   395  		paramName += ".json"
   396  	}
   397  	if val := piperenv.GetResourceParameter(path, res.Name, paramName); len(val) > 0 {
   398  		if param.Type != "string" {
   399  			var unmarshalledValue interface{}
   400  			err := json.Unmarshal([]byte(val), &unmarshalledValue)
   401  			if err != nil {
   402  				log.Entry().Debugf("Failed to unmarshal: %v", val)
   403  			}
   404  			return unmarshalledValue
   405  		}
   406  		return val
   407  	}
   408  	return nil
   409  }
   410  
   411  // GetReference returns the ResourceReference of the given type
   412  func (m *StepParameters) GetReference(refType string) *ResourceReference {
   413  	for _, ref := range m.ResourceRef {
   414  		if refType == ref.Type {
   415  			return &ref
   416  		}
   417  	}
   418  	return nil
   419  }
   420  
   421  func getFilterForResourceReferences(params []StepParameters) []string {
   422  	var filter []string
   423  	for _, param := range params {
   424  		reference := param.GetReference("vaultSecret")
   425  		if reference == nil {
   426  			reference = param.GetReference("vaultSecretFile")
   427  		}
   428  		if reference == nil {
   429  			return filter
   430  		}
   431  		if reference.Name != "" {
   432  			filter = append(filter, reference.Name)
   433  		}
   434  	}
   435  	return filter
   436  }
   437  
   438  // HasReference checks whether StepData contains a parameter that has Reference with the given type
   439  func (m *StepData) HasReference(refType string) bool {
   440  	for _, param := range m.Spec.Inputs.Parameters {
   441  		if param.GetReference(refType) != nil {
   442  			return true
   443  		}
   444  	}
   445  	return false
   446  }
   447  
   448  // EnvVarsAsMap converts container EnvVars into a map as required by dockerExecute
   449  func EnvVarsAsMap(envVars []EnvVar) map[string]string {
   450  	e := map[string]string{}
   451  	for _, v := range envVars {
   452  		e[v.Name] = v.Value
   453  	}
   454  	return e
   455  }
   456  
   457  // OptionsAsStringSlice converts container options into a string slice as required by dockerExecute
   458  func OptionsAsStringSlice(options []Option) []string {
   459  	e := []string{}
   460  	for _, v := range options {
   461  		if len(v.Value) != 0 {
   462  			e = append(e, fmt.Sprintf("%v %v", v.Name, v.Value))
   463  		} else {
   464  			e = append(e, fmt.Sprintf("%v=", v.Name))
   465  		}
   466  
   467  	}
   468  	return e
   469  }
   470  
   471  func putStringIfNotEmpty(config map[string]interface{}, key, value string) {
   472  	if value != "" {
   473  		config[key] = value
   474  	}
   475  }
   476  
   477  func putMapIfNotEmpty(config map[string]interface{}, key string, value map[string]string) {
   478  	if len(value) > 0 {
   479  		config[key] = value
   480  	}
   481  }
   482  
   483  func putSliceIfNotEmpty(config map[string]interface{}, key string, value []string) {
   484  	if len(value) > 0 {
   485  		config[key] = value
   486  	}
   487  }
   488  
   489  func ResolveMetadata(gitHubTokens map[string]string, metaDataResolver func() map[string]StepData, stepMetadata string, stepName string) (StepData, error) {
   490  
   491  	var metadata StepData
   492  
   493  	if stepMetadata != "" {
   494  		metadataFile, err := OpenPiperFile(stepMetadata, gitHubTokens)
   495  		if err != nil {
   496  			return metadata, errors.Wrap(err, "open failed")
   497  		}
   498  
   499  		err = metadata.ReadPipelineStepData(metadataFile)
   500  		if err != nil {
   501  			return metadata, errors.Wrap(err, "read failed")
   502  		}
   503  	} else {
   504  		if stepName != "" {
   505  			if metaDataResolver == nil {
   506  				return metadata, errors.New("metaDataResolver is nil")
   507  			}
   508  			metadataMap := metaDataResolver()
   509  			var ok bool
   510  			metadata, ok = metadataMap[stepName]
   511  			if !ok {
   512  				return metadata, errors.Errorf("could not retrieve by stepName %v", stepName)
   513  			}
   514  		} else {
   515  			return metadata, errors.Errorf("either one of stepMetadata or stepName parameter has to be passed")
   516  		}
   517  	}
   518  	return metadata, nil
   519  }
   520  
   521  //ToDo: Enable this when the Volumes part is also implemented
   522  //func volumeMountsAsStringSlice(volumeMounts []VolumeMount) []string {
   523  //	e := []string{}
   524  //	for _, v := range volumeMounts {
   525  //		e = append(e, fmt.Sprintf("%v:%v", v.Name, v.MountPath))
   526  //	}
   527  //	return e
   528  //}