github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/bitbucket/converter.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 bitbucket 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "os" 22 "strings" 23 24 bitbucket "github.com/drone/go-convert/convert/bitbucket/yaml" 25 harness "github.com/drone/spec/dist/go" 26 27 "github.com/drone/go-convert/internal/store" 28 "github.com/ghodss/yaml" 29 ) 30 31 // Converter converts a Bitbucket pipeline to a harness 32 // v1 pipeline. 33 type Converter struct { 34 kubeEnabled bool 35 kubeNamespace string 36 kubeConnector string 37 dockerhubConn string 38 identifiers *store.Identifiers 39 40 // as we walk the yaml, we store a 41 // a snapshot of the current node and 42 // its parents. 43 config *bitbucket.Config 44 stage *bitbucket.Stage 45 steps *bitbucket.Steps 46 step *bitbucket.Step 47 script *bitbucket.Script 48 } 49 50 // New creates a new Converter that converts a Bitbucket 51 // pipeline to a harness v1 pipeline. 52 func New(options ...Option) *Converter { 53 d := new(Converter) 54 55 // create the unique identifier store. this store 56 // is used for registering unique identifiers to 57 // prevent duplicate names, unique index violations. 58 d.identifiers = store.New() 59 60 // loop through and apply the options. 61 for _, option := range options { 62 option(d) 63 } 64 65 // set the default kubernetes namespace. 66 if d.kubeNamespace == "" { 67 d.kubeNamespace = "default" 68 } 69 70 // set the runtime to kubernetes if the kubernetes 71 // connector is configured. 72 if d.kubeConnector != "" { 73 d.kubeEnabled = true 74 } 75 76 return d 77 } 78 79 // Convert downgrades a v1 pipeline. 80 func (d *Converter) Convert(r io.Reader) ([]byte, error) { 81 src, err := bitbucket.Parse(r) 82 if err != nil { 83 return nil, err 84 } 85 d.config = src // push the bitbucket config to the state 86 return d.convert() 87 } 88 89 // ConvertString downgrades a v1 pipeline. 90 func (d *Converter) ConvertBytes(b []byte) ([]byte, error) { 91 return d.Convert( 92 bytes.NewBuffer(b), 93 ) 94 } 95 96 // ConvertString downgrades a v1 pipeline. 97 func (d *Converter) ConvertString(s string) ([]byte, error) { 98 return d.Convert( 99 bytes.NewBufferString(s), 100 ) 101 } 102 103 // ConvertFile downgrades a v1 pipeline. 104 func (d *Converter) ConvertFile(p string) ([]byte, error) { 105 f, err := os.Open(p) 106 if err != nil { 107 return nil, err 108 } 109 defer f.Close() 110 return d.Convert(f) 111 } 112 113 // converts converts a bitbucket pipeline pipeline. 114 func (d *Converter) convert() ([]byte, error) { 115 116 // normalize the yaml and ensure 117 // all root-level steps are grouped 118 // by stage to simplify conversion. 119 bitbucket.Normalize(d.config) 120 121 // create the harness pipeline spec 122 pipeline := &harness.Pipeline{ 123 Options: convertDefault(d.config), 124 } 125 126 // create the harness pipeline resource 127 config := &harness.Config{ 128 Version: 1, 129 Kind: "pipeline", 130 Spec: pipeline, 131 } 132 133 for _, steps := range d.config.Pipelines.Default { 134 if steps.Stage != nil { 135 // TODO support for fast-fail 136 d.stage = steps.Stage // push the stage to the state 137 stage := d.convertStage() 138 pipeline.Stages = append(pipeline.Stages, stage) 139 } 140 } 141 142 // marshal the harness yaml 143 out, err := yaml.Marshal(config) 144 if err != nil { 145 return nil, err 146 } 147 148 return out, nil 149 } 150 151 // helper function converts a bitbucket stage to 152 // a harness stage. 153 func (d *Converter) convertStage() *harness.Stage { 154 155 // create the harness stage spec 156 spec := &harness.StageCI{ 157 Clone: convertClone(d.stage), 158 // TODO Repository 159 // TODO Delegate 160 // TODO Platform 161 // TODO Runtime 162 // TODO Envs 163 } 164 165 // find the step with the largest size and use that 166 // size. else fallback to the global size. 167 if size := extractSize(d.config.Options, d.stage); size != bitbucket.SizeNone { 168 spec.Runtime = &harness.Runtime{ 169 Type: "cloud", 170 Spec: &harness.RuntimeCloud{ 171 Size: convertSize(size), 172 }, 173 } 174 } 175 176 // find the unique cache paths used by this 177 // stage and setup harness caching 178 if paths := extractCache(d.stage); len(paths) != 0 { 179 spec.Cache = convertCache(d.config.Definitions, paths) 180 } 181 182 // find the unique services used by this stage and 183 // setup the relevant background steps 184 if services := extractServices(d.stage); len(services) != 0 { 185 spec.Steps = append(spec.Steps, d.convertServices(services)...) 186 } 187 188 // create the harness stage. 189 stage := &harness.Stage{ 190 Name: "build", 191 Type: "ci", 192 Spec: spec, 193 // TODO When 194 // TODO Failure 195 } 196 197 // find the unique selectors and append 198 // to the stage. 199 if runson := extractRunsOn(d.stage); len(runson) != 0 { 200 stage.Delegate = runson 201 } 202 203 // default docker service (container-based only) 204 if d.config.Options != nil && d.config.Options.Docker { 205 spec.Steps = append(spec.Steps, &harness.Step{ 206 Name: d.identifiers.Generate("dind", "service"), 207 Type: "background", 208 Spec: &harness.StepBackground{ 209 Image: "docker:dind", 210 Ports: []string{"2375", "2376"}, 211 Network: "host", // TODO host networking for cloud only 212 Privileged: true, 213 }, 214 }) 215 } 216 217 // default services 218 // TODO 219 220 for _, steps := range d.stage.Steps { 221 if steps.Parallel != nil { 222 // TODO parallel steps 223 // TODO fast fail 224 d.steps = steps // push the parallel step to the state 225 step := d.convertParallel() 226 spec.Steps = append(spec.Steps, step) 227 } 228 if steps.Step != nil { 229 d.step = steps.Step // push the step to the state 230 step := d.convertStep() 231 spec.Steps = append(spec.Steps, step) 232 } 233 } 234 235 // if the stage has a single step, and that step is a 236 // group step, we can eliminate the un-necessary group 237 // and add the steps directly to the stage. 238 if len(spec.Steps) == 1 { 239 if group, ok := spec.Steps[0].Spec.(*harness.StepGroup); ok { 240 spec.Steps = group.Steps 241 } 242 } 243 244 return stage 245 } 246 247 // helper function converts global bitbucket services to 248 // harness background steps. The list of global bitbucket 249 // services is filtered by the services string slice. 250 func (d *Converter) convertServices(services []string) []*harness.Step { 251 var steps []*harness.Step 252 253 // if no global services defined, exit 254 if d.config.Definitions == nil { 255 return nil 256 } 257 258 // iterate through services and create background steps 259 for _, name := range services { 260 // lookup the service and skip if not found, 261 // or if there is no image definition 262 service, ok := d.config.Definitions.Services[name] 263 if !ok { 264 continue 265 } else if service.Image == nil { 266 continue 267 } 268 269 spec := &harness.StepBackground{ 270 Image: service.Image.Name, 271 Envs: service.Variables, 272 Network: "host", // TODO host netowrking for cloud only 273 } 274 275 // if the service is of type docker, we 276 // should open up the default docker ports 277 // and also run in privileged mode. 278 if service.Type == "docker" { 279 spec.Privileged = true 280 spec.Ports = []string{"2375", "2376"} // TODO can we remove this? 281 spec.Network = "host" // TODO host networking for Cloud only 282 } 283 284 // if the service specifies a uid then set the 285 // step user identifier. 286 if v := service.Image.RunAsUser; v != 0 { 287 spec.User = fmt.Sprint(v) 288 } 289 290 // if the service defines memory set the 291 // harness resource limit. 292 if v := service.Memory; v != 0 { 293 // memory in bitbucket is measured in megabytes 294 // so we need to convert to bytes. 295 spec.Resources = &harness.Resources{ 296 Limits: &harness.Resource{ 297 Memory: harness.MemStringorInt(v * 1000000), 298 }, 299 } 300 } 301 302 step := &harness.Step{ 303 Name: d.identifiers.Generate(name, "service"), 304 Type: "background", 305 Spec: spec, 306 } 307 308 steps = append(steps, step) 309 } 310 return steps 311 } 312 313 // helper function converts a bitbucket parallel step 314 // group to a Harness parallel step group. 315 func (d *Converter) convertParallel() *harness.Step { 316 317 // create the step group spec 318 spec := new(harness.StepParallel) 319 320 for _, src := range d.steps.Parallel.Steps { 321 if src.Step != nil { 322 d.step = src.Step 323 step := d.convertStep() 324 spec.Steps = append(spec.Steps, step) 325 } 326 } 327 328 // else create the step group wrapper. 329 return &harness.Step{ 330 Type: "parallel", 331 Spec: spec, 332 Name: d.identifiers.Generate("parallel", "parallel"), // TODO can we avoid a name here? 333 } 334 } 335 336 // helper function converts a bitbucket step 337 // to a harness run step or plugin step. 338 func (d *Converter) convertStep() *harness.Step { 339 // create the step group spec 340 spec := new(harness.StepGroup) 341 342 // loop through each script item 343 for _, script := range d.step.Script { 344 d.script = script 345 346 // if a pipe step 347 if script.Pipe != nil { 348 step := d.convertPipeStep() 349 spec.Steps = append(spec.Steps, step) 350 } 351 352 // else if a script step 353 if script.Pipe == nil { 354 step := d.convertScriptStep() 355 spec.Steps = append(spec.Steps, step) 356 } 357 } 358 359 // and loop through each after script item 360 for _, script := range d.step.ScriptAfter { 361 d.script = script 362 363 // if a pipe step 364 if script.Pipe != nil { 365 step := d.convertPipeStep() 366 spec.Steps = append(spec.Steps, step) 367 } 368 369 // else if a script step 370 if script.Pipe == nil { 371 step := d.convertScriptStep() 372 spec.Steps = append(spec.Steps, step) 373 } 374 } 375 376 // if there is only a single step, no need to 377 // create a step group. 378 if len(spec.Steps) == 1 { 379 return spec.Steps[0] 380 } 381 382 // else create the step group wrapper. 383 return &harness.Step{ 384 Type: "group", 385 Spec: spec, 386 Name: d.identifiers.Generate(d.step.Name, "group"), 387 } 388 } 389 390 // helper function converts a script step to a 391 // harness run step. 392 func (d *Converter) convertScriptStep() *harness.Step { 393 394 // create the run spec 395 spec := &harness.StepExec{ 396 Run: d.script.Text, 397 398 // TODO configure an optional connector 399 // TODO configure pull policy 400 // TODO configure envs 401 // TODO configure volumes 402 // TODO configure resources 403 } 404 405 // use the global image, if set 406 if image := d.config.Image; image != nil { 407 spec.Image = strings.TrimPrefix(image.Name, "docker://") 408 if image.RunAsUser != 0 { 409 spec.User = fmt.Sprint(image.RunAsUser) 410 } 411 } 412 413 // use the step image, if set (overrides previous) 414 if image := d.step.Image; image != nil { 415 spec.Image = strings.TrimPrefix(image.Name, "docker://") 416 if image.RunAsUser != 0 { 417 spec.User = fmt.Sprint(image.RunAsUser) 418 } 419 } 420 421 // create the run step wrapper 422 step := &harness.Step{ 423 Type: "script", 424 Spec: spec, 425 Name: d.identifiers.Generate(d.step.Name, "run"), 426 } 427 428 // use the global max-time, if set 429 if d.config.Options != nil { 430 if v := int64(d.config.Options.MaxTime); v != 0 { 431 step.Timeout = minuteToDurationString(v) 432 } 433 } 434 435 // set the timeout 436 if v := int64(d.step.MaxTime); v != 0 { 437 step.Timeout = minuteToDurationString(v) 438 } 439 440 return step 441 } 442 443 // helper function converts a pipe step to a 444 // harness plugin step. 445 func (d *Converter) convertPipeStep() *harness.Step { 446 pipe := d.script.Pipe 447 448 // create the plugin spec 449 spec := &harness.StepPlugin{ 450 Image: strings.TrimPrefix(pipe.Image, "docker://"), 451 452 // TODO configure an optional connector 453 // TODO configure envs 454 // TODO configure volumes 455 } 456 457 // append the plugin spec variables 458 spec.With = map[string]interface{}{} 459 for key, val := range pipe.Variables { 460 spec.With[key] = val 461 } 462 463 // create the plugin step wrapper 464 step := &harness.Step{ 465 Type: "plugin", 466 Spec: spec, 467 Name: d.identifiers.Generate(d.step.Name, "plugin"), 468 } 469 470 // set the timeout 471 if v := int64(d.step.MaxTime); v != 0 { 472 step.Timeout = minuteToDurationString(v) 473 } 474 475 return step 476 }