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 //}