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  }