github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/circle/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 circle converts Circle pipelines to Harness pipelines. 16 package circle 17 18 import ( 19 "bytes" 20 "errors" 21 "fmt" 22 "io" 23 "os" 24 "strings" 25 26 "github.com/drone/go-convert/convert/circle/internal/orbs" 27 circle "github.com/drone/go-convert/convert/circle/yaml" 28 harness "github.com/drone/spec/dist/go" 29 30 "github.com/drone/go-convert/internal/store" 31 "github.com/ghodss/yaml" 32 ) 33 34 // Converter converts a Circle pipeline to a harness 35 // v1 pipeline. 36 type Converter struct { 37 kubeEnabled bool 38 kubeNamespace string 39 kubeConnector string 40 gcsBucket string 41 gcsToken string 42 gcsEnabled bool 43 s3Enabled bool 44 s3Bucket string 45 s3Region string 46 s3AccessKey string 47 s3SecretKey string 48 dockerhubConn string 49 identifiers *store.Identifiers 50 } 51 52 // New creates a new Converter that converts a Circle 53 // pipeline to a harness v1 pipeline. 54 func New(options ...Option) *Converter { 55 d := new(Converter) 56 57 // create the unique identifier store. this store 58 // is used for registering unique identifiers to 59 // prevent duplicate names, unique index violations. 60 d.identifiers = store.New() 61 62 // loop through and apply the options. 63 for _, option := range options { 64 option(d) 65 } 66 67 // set the default kubernetes namespace. 68 if d.kubeNamespace == "" { 69 d.kubeNamespace = "default" 70 } 71 72 // set the runtime to kubernetes if the kubernetes 73 // connector is configured. 74 if d.kubeConnector != "" { 75 d.kubeEnabled = true 76 } 77 78 // set the storage engine to s3 if configured. 79 if d.s3Bucket != "" { 80 d.s3Enabled = true 81 } 82 83 // set the storage engine to gcs if configured. 84 if d.gcsBucket != "" { 85 d.gcsEnabled = true 86 } 87 88 return d 89 } 90 91 // Convert downgrades a v1 pipeline. 92 func (d *Converter) Convert(r io.Reader) ([]byte, error) { 93 src, err := circle.Parse(r) 94 if err != nil { 95 return nil, err 96 } 97 return d.convert(src) 98 } 99 100 // ConvertString downgrades a v1 pipeline. 101 func (d *Converter) ConvertBytes(b []byte) ([]byte, error) { 102 return d.Convert( 103 bytes.NewBuffer(b), 104 ) 105 } 106 107 // ConvertString downgrades a v1 pipeline. 108 func (d *Converter) ConvertString(s string) ([]byte, error) { 109 return d.Convert( 110 bytes.NewBufferString(s), 111 ) 112 } 113 114 // ConvertFile downgrades a v1 pipeline. 115 func (d *Converter) ConvertFile(p string) ([]byte, error) { 116 f, err := os.Open(p) 117 if err != nil { 118 return nil, err 119 } 120 defer f.Close() 121 return d.Convert(f) 122 } 123 124 // converts converts a circle pipeline pipeline. 125 func (d *Converter) convert(config *circle.Config) ([]byte, error) { 126 127 // create the harness pipeline spec 128 pipeline := &harness.Pipeline{} 129 130 // convert pipeline and job parameters to inputs 131 if params := extractParameters(config); len(params) != 0 { 132 pipeline.Inputs = convertParameters(params) 133 } 134 135 // require a minimum of 1 workflows 136 if config.Workflows == nil || len(config.Workflows.Items) == 0 { 137 return nil, errors.New("no workflows defined") 138 } 139 140 // choose the first workflow in the list, for now 141 var pipelines []*harness.Pipeline 142 for _, workflow := range config.Workflows.Items { 143 pipelines = append(pipelines, d.convertPipeline(workflow, config)) 144 } 145 146 var buf bytes.Buffer 147 for i, pipeline := range pipelines { 148 // marshal the harness yaml 149 out, err := yaml.Marshal(&harness.Config{ 150 Version: 1, 151 Kind: "pipeline", 152 Spec: pipeline, 153 }) 154 if err != nil { 155 return nil, err 156 } 157 158 // replace circle parameters with harness parameters 159 out = replaceParams(out, params) 160 161 // write the pipeline to the buffer. if there are 162 // multiple yaml files they need to be separated 163 // by the document separator. 164 if i > 0 { 165 buf.WriteString("\n") 166 buf.WriteString("---") 167 buf.WriteString("\n") 168 } 169 buf.Write(out) 170 } 171 172 return buf.Bytes(), nil 173 } 174 175 // converts converts a circle pipeline pipeline. 176 func (d *Converter) convertPipeline(workflow *circle.Workflow, config *circle.Config) *harness.Pipeline { 177 178 // create the harness pipeline spec 179 pipeline := &harness.Pipeline{} 180 181 // convert pipeline and job parameters to inputs 182 if params := extractParameters(config); len(params) != 0 { 183 pipeline.Inputs = convertParameters(params) 184 } 185 186 // loop through workflow jobs and convert each 187 // job to a stage. 188 for _, workflowjob := range workflow.Jobs { 189 // snapshot the config 190 config_ := config 191 192 // lookup the named job 193 job, ok := config_.Jobs[workflowjob.Name] 194 if !ok { 195 // if the job does not exist, check to 196 // see if the job is an orb. 197 alias, command := splitOrb(workflowjob.Name) 198 199 // lookup the orb and silently skip the 200 // job if not found 201 orb, ok := config_.Orbs[alias] 202 if !ok { 203 continue 204 } 205 206 // HACK (bradrydzewski) this is a temporary 207 // hack to create the configuration for an 208 // orb referenced directly in the workflow. 209 if orb.Inline == nil { 210 // config_ = new(circle.Config) 211 // config_.Orbs = map[string]*circle.Orb{ 212 // orb.Name: {}, 213 // } 214 job = &circle.Job{ 215 Steps: []*circle.Step{ 216 { 217 Custom: &circle.Custom{ 218 Name: workflowjob.Name, 219 Params: workflowjob.Params, 220 }, 221 }, 222 }, 223 } 224 } else { 225 // lookup the orb command and silently skip 226 // the job if not found 227 job, ok = orb.Inline.Jobs[command] 228 if !ok { 229 continue 230 } 231 232 // replace the config_ with the orb 233 config_ = orb.Inline 234 } 235 } 236 237 // this section replaces circle matrix expressions 238 // with harness circle matrix expressions. 239 // 240 // before: << parameters.foo >> 241 // after: << matrix.foo >> 242 replaceParamsMatrix(job, workflowjob.Matrix) 243 244 // convert the circle job to a stage and silently 245 // skip any stages that cannot be converted. 246 stage := d.convertStage(job, config_) 247 if stage == nil { 248 continue 249 } 250 251 stage.Name = workflowjob.Name 252 253 if v := workflowjob.Matrix; v != nil { 254 stage.Strategy = convertMatrix(job, v) 255 } 256 257 // TODO workflows.[*].triggers 258 // TODO workflows.[*].unless 259 // TODO workflows.[*].when 260 // TODO workflows.[*].jobs[*].context 261 // TODO workflows.[*].jobs[*].filters 262 // TODO workflows.[*].jobs[*].type 263 // TODO workflows.[*].jobs[*].requires 264 265 // append the converted stage to the pipeline. 266 pipeline.Stages = append(pipeline.Stages, stage) 267 } 268 269 return pipeline 270 } 271 272 // helper function converts Circle job to a Harness stage. 273 func (d *Converter) convertStage(job *circle.Job, config *circle.Config) *harness.Stage { 274 275 // create stage spec 276 spec := &harness.StageCI{ 277 Envs: job.Environment, 278 Platform: convertPlatform(job, config), 279 Runtime: convertRuntime(job, config), 280 Steps: append( 281 defaultBackgroundSteps(job, config), 282 d.convertSteps(job.Steps, job, config)..., 283 ), 284 } 285 286 // TODO executor.machine 287 // TODO executor.shell 288 // TODO executor.working_directory 289 290 // if there are no steps in the stage we 291 // can skip adding the stage to the pipeline. 292 if len(spec.Steps) == 0 { 293 return nil 294 } 295 296 // TODO job.branches 297 // TODO job.parallelism 298 // TODO job.parameters 299 300 optimizeCache(spec) 301 optimizeGroup(spec) 302 303 // create the stage 304 stage := &harness.Stage{} 305 stage.Type = "ci" 306 stage.Spec = spec 307 return stage 308 } 309 310 // helper function converts Circle steps to Harness steps. 311 func (d *Converter) convertSteps(steps []*circle.Step, job *circle.Job, config *circle.Config) []*harness.Step { 312 var out []*harness.Step 313 for _, src := range steps { 314 if dst := d.convertStep(src, job, config); dst != nil { 315 out = append(out, dst) 316 } 317 } 318 return out 319 } 320 321 // helper function converts a Circle step to a Harness step. 322 func (d *Converter) convertStep(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 323 switch { 324 case step.AddSSHKeys != nil: 325 return d.convertAddSSHKeys(step) 326 case step.AttachWorkspace != nil: 327 return nil // not supported 328 case step.Checkout != nil: 329 return nil // ignore 330 case step.PersistToWorkspace != nil: 331 return nil // not supported 332 case step.RestoreCache != nil: 333 return d.convertRestoreCache(step) 334 case step.Run != nil: 335 return d.convertRun(step, job, config) 336 case step.SaveCache != nil: 337 return d.convertSaveCache(step) 338 case step.SetupRemoteDocker != nil: 339 return nil // not supported 340 case step.StoreArtifacts != nil: 341 return d.convertStoreArtifacts(step) 342 case step.StoreTestResults != nil: 343 return d.convertStoreTestResults(step) 344 case step.Unless != nil: 345 return d.convertUnlessStep(step, job, config) 346 case step.When != nil: 347 return d.convertWhenStep(step, job, config) 348 case step.Custom != nil: 349 return d.convertCustom(step, job, config) 350 default: 351 return nil 352 } 353 } 354 355 // 356 // Step Types 357 // 358 359 // helper function converts a Circle Run step. 360 func (d *Converter) convertRun(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 361 // TODO run.shell 362 // TODO run.when 363 // TODO run.working_directory 364 // TODO docker.auth.username 365 // TODO docker.auth.password 366 // TODO docker.aws_auth.aws_access_key_id 367 // TODO docker.aws_auth.aws_secret_access_key 368 369 var image string 370 var entrypoint string 371 var args []string 372 var user string 373 var envs map[string]string 374 var shell string 375 376 if docker := extractDocker(job, config); docker != nil { 377 image = docker.Image 378 entrypoint = "" // TODO needs a Harness v1 spec change 379 args = docker.Command 380 user = docker.User 381 envs = docker.Environment 382 } 383 384 runCommand := step.Run.Command 385 if job.Shell != "" { 386 shellOptions := strings.Split(job.Shell, " ")[1:] // split the shell options from the shell binary 387 if len(shellOptions) > 0 { 388 shellOptionStr := strings.Join(shellOptions, " ") // join the shell options back into a single string 389 runCommand = fmt.Sprintf("set %s\n%s", shellOptionStr, runCommand) // prepend the shell options to the run command 390 } 391 shell = strings.Split(job.Shell, " ")[0] 392 shell = strings.Split(shell, "/")[len(strings.Split(shell, "/"))-1] 393 } else { // default shell 394 shell = "bash" 395 runCommand = "set -eo pipefail\n" + runCommand 396 } 397 398 if step.Run.Background { 399 return &harness.Step{ 400 Name: step.Run.Name, 401 Type: "background", 402 Spec: &harness.StepBackground{ 403 Run: runCommand, 404 Envs: combineEnvs(step.Run.Environment, envs), 405 Image: image, 406 Entrypoint: entrypoint, 407 Args: args, 408 User: user, 409 Shell: shell, 410 }, 411 } 412 } else { 413 return &harness.Step{ 414 Name: step.Run.Name, 415 Type: "script", 416 Spec: &harness.StepExec{ 417 Run: runCommand, 418 Envs: combineEnvs(step.Run.Environment, envs), 419 Image: image, 420 Entrypoint: entrypoint, 421 Args: args, 422 User: user, 423 Shell: shell, 424 }, 425 } 426 } 427 } 428 429 // helper function converts a Circle Restore Cache step. 430 func (d *Converter) convertRestoreCache(step *circle.Step) *harness.Step { 431 // TODO support restore_cache.keys (plural) 432 return &harness.Step{ 433 Name: d.identifiers.Generate(step.RestoreCache.Name, "restore_cache"), 434 Type: "plugin", 435 Spec: &harness.StepPlugin{ 436 Image: "plugins/cache", 437 With: map[string]interface{}{ 438 "bucket": `<+ secrets.getValue("aws_bucket") >`, 439 "region": `<+ secrets.getValue("aws_region") >`, 440 "access_key": `<+ secrets.getValue("aws_access_key_id") >`, 441 "secret_key": `<+ secrets.getValue("aws_secret_access_key") >`, 442 "cache_key": step.RestoreCache.Key, 443 "restore": "true", 444 "exit_code": "true", 445 "archive_format": "tar", 446 "backend": "s3", 447 "backend_operation_timeout": "1800s", 448 "fail_restore_if_key_not_present": "false", 449 }, 450 }, 451 } 452 } 453 454 // helper function converts a Save Cache step. 455 func (d *Converter) convertSaveCache(step *circle.Step) *harness.Step { 456 // TODO support save_cache.when 457 return &harness.Step{ 458 Name: d.identifiers.Generate(step.SaveCache.Name, "save_cache"), 459 Type: "plugin", 460 Spec: &harness.StepPlugin{ 461 Image: "plugins/cache", 462 With: map[string]interface{}{ 463 "bucket": `<+ secrets.getValue("aws_bucket") >`, 464 "region": `<+ secrets.getValue("aws_region") >`, 465 "access_key": `<+ secrets.getValue("aws_access_key_id") >`, 466 "secret_key": `<+ secrets.getValue("aws_secret_access_key") >`, 467 "cache_key": step.SaveCache.Key, 468 "rebuild": "true", 469 "mount": step.SaveCache.Paths, 470 "exit_code": "true", 471 "archive_format": "tar", 472 "backend": "s3", 473 "backend_operation_timeout": "1800s", 474 "fail_restore_if_key_not_present": "false", 475 }, 476 }, 477 } 478 } 479 480 // helper function converts a Add SSH Keys step. 481 func (d *Converter) convertAddSSHKeys(step *circle.Step) *harness.Step { 482 // TODO step.AddSSHKeys.Fingerprints 483 return &harness.Step{ 484 Name: d.identifiers.Generate(step.AddSSHKeys.Name, "add_ssh_keys"), 485 Type: "script", 486 Spec: &harness.StepExec{ 487 Run: "echo unable to convert add_ssh_keys step", 488 }, 489 } 490 } 491 492 // helper function converts a Store Artifacts step. 493 func (d *Converter) convertStoreArtifacts(step *circle.Step) *harness.Step { 494 src := step.StoreArtifacts.Path 495 dst := step.StoreArtifacts.Destination 496 if dst == "" { 497 dst = "/" 498 } 499 return &harness.Step{ 500 Name: d.identifiers.Generate(step.StoreArtifacts.Name, "store_artifacts"), 501 Type: "plugin", 502 Spec: &harness.StepPlugin{ 503 Image: "plugins/s3", 504 With: map[string]interface{}{ 505 "bucket": `<+ secrets.getValue("aws_bucket") >`, 506 "region": `<+ secrets.getValue("aws_region") >`, 507 "access_key": `<+ secrets.getValue("aws_access_key_id") >`, 508 "secret_key": `<+ secrets.getValue("aws_secret_access_key") >`, 509 "source": src, 510 "target": dst, 511 }, 512 }, 513 } 514 } 515 516 // helper function converts a Test Results step. 517 func (d *Converter) convertStoreTestResults(step *circle.Step) *harness.Step { 518 return &harness.Step{ 519 Name: d.identifiers.Generate(step.StoreTestResults.Name, "store_test_results"), 520 Type: "script", 521 Spec: &harness.StepExec{ 522 Run: "echo upload unit test results", 523 Reports: []*harness.Report{ 524 { 525 Path: []string{step.StoreTestResults.Path}, 526 Type: "junit", 527 }, 528 }, 529 }, 530 } 531 } 532 533 // helper function converts a When step. 534 func (d *Converter) convertWhenStep(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 535 steps := d.convertSteps(step.When.Steps, job, config) 536 if len(steps) == 0 { 537 return nil 538 } 539 // TODO step.When.Condition 540 return &harness.Step{ 541 Type: "group", 542 Spec: &harness.StepGroup{ 543 Steps: steps, 544 }, 545 } 546 } 547 548 // helper function converts an Unless step. 549 func (d *Converter) convertUnlessStep(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 550 steps := d.convertSteps(step.Unless.Steps, job, config) 551 if len(steps) == 0 { 552 return nil 553 } 554 // TODO step.Unless.Condition 555 return &harness.Step{ 556 Type: "group", 557 Spec: &harness.StepGroup{ 558 Steps: steps, 559 }, 560 } 561 } 562 563 // helper function converts a Custom step. 564 func (d *Converter) convertCustom(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 565 // check to see if the step is a re-usable command. 566 if _, ok := config.Commands[step.Custom.Name]; ok { 567 return d.convertCommand(step, job, config) 568 } 569 // else convert the orb 570 return d.convertOrb(step, job, config) 571 } 572 573 // helper function converts a Command step. 574 func (d *Converter) convertCommand(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 575 // extract the command 576 command, ok := config.Commands[step.Custom.Name] 577 if !ok { 578 return nil 579 } 580 581 // find and replace parameters 582 // https://circleci.com/docs/reusing-config/#using-the-parameters-declaration 583 expandParamsCommand(command, step) 584 585 // convert the circle steps to harness steps 586 steps := d.convertSteps(command.Steps, job, config) 587 if len(steps) == 0 { 588 return nil 589 } 590 591 // If there is only one step, return it directly instead of creating a group 592 if len(steps) == 1 { 593 return steps[0] 594 } 595 596 // return a step group 597 return &harness.Step{ 598 Type: "group", 599 Spec: &harness.StepGroup{ 600 Steps: steps, 601 }, 602 } 603 } 604 605 // helper function converts an Orb step. 606 func (d *Converter) convertOrb(step *circle.Step, job *circle.Job, config *circle.Config) *harness.Step { 607 // get the orb alias and command 608 alias, command := splitOrb(step.Custom.Name) 609 610 // get the orb from the configuration 611 orb, ok := config.Orbs[alias] 612 if !ok { 613 return nil 614 } 615 616 // convert inline orbs 617 if orb.Inline != nil { 618 // use the command to get the job name 619 // if the action does not exist, silently 620 // ignore the orb. 621 job, ok := orb.Inline.Jobs[command] 622 if !ok { 623 return nil 624 } 625 // convert the orb steps to harness steps 626 // if not steps are returned, silently ignore 627 // the orb. 628 steps := d.convertSteps(job.Steps, job, orb.Inline) 629 if len(steps) == 0 { 630 return nil 631 } 632 // return a step group 633 return &harness.Step{ 634 Type: "group", 635 Spec: &harness.StepGroup{ 636 Steps: steps, 637 }, 638 } 639 } 640 641 name, version := splitOrbVersion(orb.Name) 642 643 // convert the orb 644 out := orbs.Convert(name, command, version, step.Custom) 645 if out != nil { 646 return out 647 } 648 649 return &harness.Step{ 650 Name: d.identifiers.Generate(name), 651 Type: "script", 652 Spec: &harness.StepExec{ 653 Run: fmt.Sprintf("echo unable to convert orb %s/%s", name, command), 654 }, 655 } 656 }