github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/gitlab/convert.go (about) 1 // Copyright 2022 Harness, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package gitlab converts Gitlab pipelines to Harness pipelines. 16 package gitlab 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "os" 23 "sort" 24 "strconv" 25 "strings" 26 27 gitlab "github.com/drone/go-convert/convert/gitlab/yaml" 28 "github.com/drone/go-convert/internal/store" 29 harness "github.com/drone/spec/dist/go" 30 31 "dario.cat/mergo" 32 "github.com/ghodss/yaml" 33 ) 34 35 // conversion context 36 type context struct { 37 config *gitlab.Pipeline 38 job *gitlab.Job 39 } 40 41 // Converter converts a Gitlab pipeline to a Harness 42 // v1 pipeline. 43 type Converter struct { 44 kubeEnabled bool 45 kubeNamespace string 46 kubeConnector string 47 dockerhubConn string 48 identifiers *store.Identifiers 49 50 // config *gitlab.Pipeline 51 // job *gitlab.Job 52 } 53 54 // New creates a new Converter that converts a GitLab 55 // pipeline to a Harness v1 pipeline. 56 func New(options ...Option) *Converter { 57 d := new(Converter) 58 59 // create the unique identifier store. this store 60 // is used for registering unique identifiers to 61 // prevent duplicate names, unique index violations. 62 d.identifiers = store.New() 63 64 // loop through and apply the options. 65 for _, option := range options { 66 option(d) 67 } 68 69 // set the default kubernetes namespace. 70 if d.kubeNamespace == "" { 71 d.kubeNamespace = "default" 72 } 73 74 // set the runtime to kubernetes if the kubernetes 75 // connector is configured. 76 if d.kubeConnector != "" { 77 d.kubeEnabled = true 78 } 79 80 return d 81 } 82 83 // Convert downgrades a v1 pipeline. 84 func (d *Converter) Convert(r io.Reader) ([]byte, error) { 85 src, err := gitlab.Parse(r) 86 if err != nil { 87 return nil, err 88 } 89 return d.convert(&context{ 90 config: src, 91 }) 92 } 93 94 // ConvertBytes downgrades a v1 pipeline. 95 func (d *Converter) ConvertBytes(b []byte) ([]byte, error) { 96 return d.Convert( 97 bytes.NewBuffer(b), 98 ) 99 } 100 101 // ConvertString downgrades a v1 pipeline. 102 func (d *Converter) ConvertString(s string) ([]byte, error) { 103 return d.Convert( 104 bytes.NewBufferString(s), 105 ) 106 } 107 108 // ConvertFile downgrades a v1 pipeline. 109 func (d *Converter) ConvertFile(p string) ([]byte, error) { 110 f, err := os.Open(p) 111 if err != nil { 112 return nil, err 113 } 114 defer f.Close() 115 return d.Convert(f) 116 } 117 118 // converts converts a GitLab pipeline to a Harness pipeline. 119 func (d *Converter) convert(ctx *context) ([]byte, error) { 120 121 // create the harness pipeline spec 122 dst := &harness.Pipeline{} 123 124 // create the harness pipeline resource 125 config := &harness.Config{ 126 Version: 1, 127 Kind: "pipeline", 128 Spec: dst, 129 } 130 131 cacheFound := false 132 133 // TODO handle includes 134 // src.Include 135 136 if ctx.config.Workflow != nil { 137 // TODO pipeline.name removed from spec 138 // dst.Name = ctx.config.Workflow.Name 139 } 140 141 // create the harness stage. 142 dstStage := &harness.Stage{ 143 Name: "test", 144 Type: "ci", 145 // When: convertCond(from.Trigger), 146 Spec: &harness.StageCI{ 147 // Delegate: convertNode(from.Node), 148 Envs: convertVariables(ctx.config.Variables), 149 // Platform: convertPlatform(from.Platform), 150 // Runtime: convertRuntime(from), 151 Steps: make([]*harness.Step, 0), // Initialize the Steps slice 152 }, 153 } 154 dst.Stages = append(dst.Stages, dstStage) 155 156 var jobKeys []string 157 for jobKey := range ctx.config.Jobs { 158 jobKeys = append(jobKeys, jobKey) 159 } 160 sort.Strings(jobKeys) 161 162 stages := ctx.config.Stages 163 stagesLength := len(stages) 164 if stagesLength == 0 { 165 stages = []string{".pre", "build", "test", "deploy", ".post"} // stages don't have to be declared for valid yaml. Default to test 166 } else { 167 dstStage.Name = ctx.config.Stages[0] 168 } 169 170 for _, stageName := range stages { 171 stageSteps := make([]*harness.Step, 0) 172 173 // maintain stage name if set 174 if stagesLength != 0 { 175 dstStage.Name = stageName 176 } 177 178 // iterate through jobs and find jobs assigned to the stage. Skip other stages. 179 for _, jobName := range jobKeys { 180 job := ctx.config.Jobs[jobName] // maintaining order here 181 if job.Before != nil { 182 job.Stage = ".pre" 183 } 184 if job.After != nil { 185 job.Stage = ".post" 186 } 187 if job.Stage == "" { 188 job.Stage = "test" // default 189 } 190 if !cacheFound && job.Cache != nil { 191 dstStage.Spec.(*harness.StageCI).Cache = convertCache(job.Cache) // Update cache if it's defined in the job 192 cacheFound = true 193 } 194 195 if len(job.Extends) > 0 { 196 for _, extend := range job.Extends { 197 if templateJob, ok := ctx.config.TemplateJobs[extend]; ok { 198 // Perform deep merge of the template job into the current job. 199 mergedJob, err := mergeJobConfiguration(job, templateJob) 200 if err != nil { 201 return nil, err 202 } 203 job = mergedJob 204 } 205 } 206 } 207 208 if job == nil || job.Stage != stageName { 209 continue 210 } 211 212 if job.Parallel != nil { 213 if job.Parallel.Matrix != nil { 214 for i, matrix := range job.Parallel.Matrix { 215 steps := convertJobToStep(ctx, fmt.Sprintf("%s-%d", jobName, i), job, matrix) 216 stageSteps = append(stageSteps, steps...) 217 } 218 } 219 } else { 220 // Convert each job to a step 221 steps := convertJobToStep(ctx, jobName, job, nil) 222 for _, step := range steps { 223 // Prepend the pipeline-level before_script 224 if ctx.config.BeforeScript != nil { 225 prependScript := convertScriptToStep(ctx.config.BeforeScript, "", "", false) 226 step.Spec.(*harness.StepExec).Run = prependScript.Spec.(*harness.StepExec).Run + "\n" + step.Spec.(*harness.StepExec).Run 227 } 228 229 // Prepend the job-specific before_script 230 if job.Before != nil { 231 prependScript := convertScriptToStep(job.Before, "", "", false) 232 step.Spec.(*harness.StepExec).Run = prependScript.Spec.(*harness.StepExec).Run + "\n" + step.Spec.(*harness.StepExec).Run 233 } 234 stageSteps = append(stageSteps, step) 235 } 236 237 if job.Inherit != nil && job.Inherit.Variables != nil { 238 dstStage.Spec.(*harness.StageCI).Envs = convertInheritedVariables(job, dstStage.Spec.(*harness.StageCI).Envs) 239 } 240 } 241 } 242 // If there are multiple steps, wrap them with a parallel group to mirror gitlab behavior 243 if len(stageSteps) > 1 { 244 group := &harness.Step{ 245 Type: "parallel", 246 Spec: &harness.StepParallel{ 247 Steps: stageSteps, 248 }, 249 } 250 dstStage.Spec.(*harness.StageCI).Steps = append(dstStage.Spec.(*harness.StageCI).Steps, group) 251 } else if len(stageSteps) == 1 { 252 // If there's a single step, append it to the stage directly 253 dstStage.Spec.(*harness.StageCI).Steps = append(dstStage.Spec.(*harness.StageCI).Steps, stageSteps[0]) 254 } 255 } 256 257 // marshal the harness yaml 258 out, err := yaml.Marshal(config) 259 if err != nil { 260 return nil, err 261 } 262 263 return out, nil 264 } 265 266 // convertCache converts a GitLab cache to a Harness cache. 267 func convertCache(cache *gitlab.Cache) *harness.Cache { 268 if cache == nil { 269 return nil 270 } 271 272 return &harness.Cache{ 273 Enabled: true, 274 Key: cache.Key.Value, 275 Paths: cache.Paths, 276 Policy: cache.Policy, 277 } 278 } 279 280 // convertScriptToStep converts a GitLab script to a Harness step. 281 func convertScriptToStep(script []string, name, timeout string, onFailureIgnore bool) *harness.Step { 282 spec := new(harness.StepExec) 283 spec.Run = strings.Join(script, "\n") 284 285 step := &harness.Step{ 286 Name: name, 287 Type: "script", 288 Spec: spec, 289 } 290 if timeout != "" { 291 step.Timeout = timeout 292 } 293 if onFailureIgnore { 294 step.Failure = &harness.FailureList{ 295 Items: []*harness.Failure{ 296 { 297 Action: &harness.FailureAction{ 298 Type: "ignore", 299 }, 300 }, 301 }, 302 } 303 } 304 305 return step 306 } 307 308 // convertJobToStep converts a GitLab job to a Harness step. 309 func convertJobToStep(ctx *context, jobName string, job *gitlab.Job, matrix map[string][]string) []*harness.Step { 310 var steps []*harness.Step 311 spec := new(harness.StepExec) 312 313 if imageProvided(job.Image) { 314 spec = convertImage(job.Image) 315 } else if useDefaultImage(job, ctx) { 316 spec = convertImage(ctx.config.Default.Image) 317 } else if imageProvided(ctx.config.Image) { 318 spec = convertImage(ctx.config.Image) 319 } 320 321 if job.Inherit == nil || job.Inherit.Default == nil || job.Inherit.Default.All { 322 convertInheritDefaultFields(spec, ctx.config.Default, nil) 323 } else { 324 convertInheritDefaultFields(spec, ctx.config.Default, job.Inherit.Default.Keys) 325 } 326 327 // Convert all scripts into a single step 328 script := append(job.Script) 329 330 spec.Run = strings.Join(script, "\n") 331 332 var on *harness.FailureList 333 if job.Retry != nil { 334 on = convertRetry(job) 335 } else if job.AllowFailure != nil { 336 on = convertAllowFailure(job) 337 } 338 339 // set step environment variables 340 if job.Variables != nil || job.Secrets != nil || matrix != nil { 341 spec.Envs = make(map[string]string) 342 343 // job variables become step variables 344 if job.Variables != nil { 345 envVariables := convertVariables(job.Variables) 346 for key := range envVariables { 347 spec.Envs[key] = envVariables[key] 348 } 349 } 350 351 // job secrets become step variables that reference Harness secrets 352 if job.Secrets != nil { 353 envSecrets := convertSecrets(job.Secrets) 354 for key := range envSecrets { 355 spec.Envs[key] = envSecrets[key] 356 } 357 } 358 359 // job matrix axes become step variables that reference Harness matrix values 360 if matrix != nil { 361 envMatrix := convertVariablesMatrix(matrix) 362 for key := range envMatrix { 363 spec.Envs[key] = envMatrix[key] 364 } 365 } 366 } 367 368 var strategy *harness.Strategy 369 if matrix != nil { 370 strategy = convertStrategy(matrix) 371 } 372 373 step := &harness.Step{ 374 Name: jobName, 375 Type: "script", 376 Spec: spec, 377 } 378 // map on if exists 379 if on != nil { 380 step.Failure = on 381 } 382 // map strategy if exists 383 if strategy != nil { 384 step.Strategy = strategy 385 } 386 387 steps = append(steps, step) 388 389 // job.Cache 390 // job.Retry 391 // job.Services 392 // job.Timeout 393 // job.Tags 394 // job.Secrets 395 396 return steps 397 } 398 399 func imageProvided(image *gitlab.Image) bool { 400 return image != nil 401 } 402 403 func isInheritAll(job *gitlab.Job) bool { 404 return job.Inherit != nil && job.Inherit.Default != nil && job.Inherit.Default.All 405 } 406 407 func useDefaultImage(job *gitlab.Job, ctx *context) bool { 408 return !isInheritAll(job) && ctx.config.Default != nil && imageProvided(ctx.config.Default.Image) 409 } 410 411 // convertInheritDefaultFields converts the default fields from the default job into the current job. 412 func convertInheritDefaultFields(spec *harness.StepExec, defaultJob *gitlab.Default, keys []string) { 413 if defaultJob == nil { 414 return 415 } 416 if keys == nil { 417 keys = []string{ 418 "after_script", "before_script", "artifacts", "cache", "image", 419 "interruptible", "retry", "services", "tags", "duration", 420 } 421 } 422 for _, key := range keys { 423 switch key { 424 case "after_script": 425 if len(defaultJob.After) > 0 { 426 spec.Run = strings.Join(defaultJob.After, "\n") 427 } 428 case "before_script": 429 if len(defaultJob.Before) > 0 { 430 spec.Run = strings.Join(defaultJob.Before, "\n") + "\n" + spec.Run 431 } 432 case "artifacts": 433 if defaultJob.Artifacts != nil { 434 //TODO no supported 435 } 436 case "cache": 437 if defaultJob.Cache != nil { 438 //TODO 439 } 440 case "image": 441 if defaultJob.Image != nil { 442 spec = convertImage(defaultJob.Image) 443 } 444 case "interruptible": 445 //TODO not supported 446 case "retry": 447 if defaultJob.Retry != nil { 448 //TODO not supported 449 } 450 case "services": 451 if len(defaultJob.Services) > 0 { 452 //TODO 453 } 454 case "tags": 455 if len(defaultJob.Tags) > 0 { 456 //spec.Tags = strings.Join(defaultJob.Tags, ", ") //TODO 457 } 458 case "duration": 459 //spec.Timeout = defaultJob.Timeout //TODO 460 } 461 } 462 } 463 464 // convertInherit converts the inherit fields from the default job into the current job. 465 func convertInheritedVariables(job *gitlab.Job, stageEnvs map[string]string) map[string]string { 466 if job.Inherit == nil || job.Inherit.Variables == nil { 467 return stageEnvs 468 } 469 470 if job.Inherit.Variables.All { 471 return stageEnvs 472 } 473 474 // If inherit.variables is an array, only the variables in the array are inherited. 475 if job.Inherit.Variables.Keys != nil { 476 newEnvs := make(map[string]string) 477 for _, key := range job.Inherit.Variables.Keys { 478 if value, ok := stageEnvs[key]; ok { 479 newEnvs[key] = value 480 } 481 } 482 return newEnvs 483 } 484 485 return stageEnvs 486 } 487 488 // convertImage extracts the image name, pull policy, and entrypoint from a GitLab image. 489 func convertImage(image *gitlab.Image) *harness.StepExec { 490 spec := &harness.StepExec{} 491 492 if image != nil { 493 spec.Image = image.Name 494 if len(image.PullPolicy) == 1 { 495 pullPolicyMapping := map[string]string{ 496 "always": "always", 497 "never": "never", 498 "if-not-present": "if-not-exists", 499 } 500 501 spec.Pull = pullPolicyMapping[image.PullPolicy[0]] 502 } 503 if len(image.Entrypoint) > 0 { 504 spec.Entrypoint = image.Entrypoint[0] 505 if len(image.Entrypoint) > 1 { 506 spec.Args = image.Entrypoint[1:] 507 } 508 } 509 } 510 511 return spec 512 } 513 514 func mergeJobConfiguration(child *gitlab.Job, parent *gitlab.Job) (*gitlab.Job, error) { 515 mergedJob := &gitlab.Job{} 516 517 // Copy all fields from the parent job into mergedJob. 518 if err := mergo.Merge(mergedJob, parent, mergo.WithOverride); err != nil { 519 return nil, err 520 } 521 522 // Then, copy all non-empty fields from the child job into mergedJob. 523 if err := mergo.Merge(mergedJob, child, mergo.WithOverride); err != nil { 524 return nil, err 525 } 526 527 return mergedJob, nil 528 } 529 530 // convertAllowFailure converts a GitLab job's allow_failure to a Harness step's on.failure. 531 func convertAllowFailure(job *gitlab.Job) *harness.FailureList { 532 if job.AllowFailure != nil && job.AllowFailure.Value { 533 var exitCodesStr []string 534 for _, code := range job.AllowFailure.ExitCodes { 535 exitCodesStr = append(exitCodesStr, strconv.Itoa(code)) 536 } 537 // Sort the slice to maintain order 538 sort.Strings(exitCodesStr) 539 540 on := &harness.FailureList{ 541 Items: []*harness.Failure{ 542 { 543 Errors: []string{"all"}, 544 Action: &harness.FailureAction{ 545 Type: "ignore", 546 }, 547 }, 548 }, 549 } 550 if len(exitCodesStr) > 0 { 551 // TODO exit_code needs to be re-added to spec 552 // on.Failure.ExitCodes = exitCodesStr 553 } 554 return on 555 } 556 return nil 557 } 558 559 // convertVariables converts a GitLab variables map to a Harness variables map. 560 func convertVariables(variables map[string]*gitlab.Variable) map[string]string { 561 result := make(map[string]string) 562 var keys []string 563 for key := range variables { 564 keys = append(keys, key) 565 } 566 sort.Strings(keys) 567 568 for _, key := range keys { 569 variable := variables[key] 570 if variable != nil { 571 result[key] = variable.Value 572 } 573 } 574 575 return result 576 } 577 578 // convertVariablesMatrix converts a matrix axis map to a Harness variables map. 579 func convertVariablesMatrix(axis map[string][]string) map[string]string { 580 result := make(map[string]string) 581 582 var keys []string 583 for k := range axis { 584 keys = append(keys, k) 585 } 586 sort.Strings(keys) // to maintain order 587 588 for axisName := range axis { 589 result[axisName] = fmt.Sprintf("<+matrix.%s>", axisName) 590 } 591 592 return result 593 } 594 595 // convertRetry converts a GitLab job's retry to a Harness step's on.failure.retry. 596 func convertRetry(job *gitlab.Job) *harness.FailureList { 597 if job.Retry == nil { 598 return nil 599 } 600 601 return &harness.FailureList{ 602 Items: []*harness.Failure{ 603 { 604 Action: &harness.FailureAction{ 605 Type: "retry", 606 Spec: harness.Retry{ 607 Attempts: int64(job.Retry.Max), 608 }, 609 }, 610 }, 611 }, 612 } 613 } 614 615 // convertSecrets converts a GitLab secrets map to a Harness secrets map. 616 func convertSecrets(secrets map[string]*gitlab.Secret) map[string]string { 617 result := make(map[string]string) 618 619 var keys []string 620 for k := range secrets { 621 keys = append(keys, k) 622 } 623 sort.Strings(keys) // to maintain order 624 625 for secretName := range secrets { 626 result[secretName] = fmt.Sprintf("<+secrets.getValue(\"%s\")>", secretName) 627 } 628 629 return result 630 } 631 632 func convertStrategy(axis map[string][]string) *harness.Strategy { 633 return &harness.Strategy{ 634 Type: "matrix", 635 Spec: &harness.Matrix{ 636 Axis: axis, 637 }, 638 } 639 }