github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/harness/downgrader/downgrade.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 downgrader
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"sort"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/drone/go-convert/internal/slug"
    26  	"github.com/drone/go-convert/internal/store"
    27  
    28  	harness "github.com/drone/go-convert/convert/harness/downgrader/yaml"
    29  	v0 "github.com/drone/go-convert/convert/harness/yaml"
    30  	v1 "github.com/drone/spec/dist/go"
    31  	"github.com/ghodss/yaml"
    32  )
    33  
    34  // Downgrader downgrades pipelines from the v0 harness
    35  // configuration format to the v1 configuration format.
    36  type Downgrader struct {
    37  	codebaseName  string
    38  	codebaseConn  string
    39  	dockerhubConn string
    40  	kubeConnector string
    41  	kubeNamespace string
    42  	kubeEnabled   bool
    43  	pipelineId    string
    44  	pipelineName  string
    45  	pipelineOrg   string
    46  	pipelineProj  string
    47  	identifiers   *store.Identifiers
    48  }
    49  
    50  const MaxDepth = 100
    51  
    52  var eventMap = map[string]map[string]string{
    53  	"pull_request": {
    54  		"jexl":            "<+trigger.event>",
    55  		"event":           "PR",
    56  		"operator":        "==",
    57  		"inverseOperator": "!=",
    58  	},
    59  	"push": {
    60  		"jexl":            "<+trigger.event>",
    61  		"event":           "PUSH",
    62  		"operator":        "==",
    63  		"inverseOperator": "!=",
    64  	},
    65  	"tag": {
    66  		"jexl":            "<+trigger.payload.ref>",
    67  		"event":           "refs/tags/",
    68  		"operator":        "=^",
    69  		"inverseOperator": "!^",
    70  	},
    71  }
    72  
    73  // New creates a new Downgrader that downgrades pipelines
    74  // from the v0 harness configuration format to the v1
    75  // configuration format.
    76  func New(options ...Option) *Downgrader {
    77  	d := new(Downgrader)
    78  
    79  	// create the unique identifier store. this store
    80  	// is used for registering unique identifiers to
    81  	// prevent duplicate names, unique index violations.
    82  	d.identifiers = store.New()
    83  
    84  	// loop through and apply the options.
    85  	for _, option := range options {
    86  		option(d)
    87  	}
    88  
    89  	// set the default pipeline name.
    90  	if d.pipelineName == "" {
    91  		d.pipelineName = "default"
    92  	}
    93  
    94  	// set the default pipeline id.
    95  	if d.pipelineId == "" {
    96  		d.pipelineId = slug.Create(d.pipelineName)
    97  	}
    98  
    99  	// set the default pipeline org.
   100  	if d.pipelineOrg == "" {
   101  		d.pipelineOrg = "default"
   102  	}
   103  
   104  	// set the default pipeline org.
   105  	if d.pipelineProj == "" {
   106  		d.pipelineProj = "default"
   107  	}
   108  
   109  	// set the default kubernetes namespace.
   110  	if d.kubeNamespace == "" {
   111  		d.kubeNamespace = "default"
   112  	}
   113  
   114  	// set the runtime to kubernetes if the kubernetes
   115  	// connector is configured.
   116  	if d.kubeConnector != "" {
   117  		d.kubeEnabled = true
   118  	}
   119  
   120  	return d
   121  }
   122  
   123  // Downgrade downgrades a v1 pipeline.
   124  func (d *Downgrader) Downgrade(b []byte) ([]byte, error) {
   125  	src, err := harness.ParseBytes(b)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	return d.DowngradeFrom(src)
   130  }
   131  
   132  // DowngradeString downgrades a v1 pipeline.
   133  func (d *Downgrader) DowngradeString(s string) ([]byte, error) {
   134  	return d.Downgrade([]byte(s))
   135  }
   136  
   137  // DowngradeFile downgrades a v1 pipeline.
   138  func (d *Downgrader) DowngradeFile(path string) ([]byte, error) {
   139  	out, err := ioutil.ReadFile(path)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	return d.Downgrade(out)
   144  }
   145  
   146  // DowngradeFrom downgrades a v1 pipeline object.
   147  func (d *Downgrader) DowngradeFrom(src []*v1.Config) ([]byte, error) {
   148  	return d.downgrade(src)
   149  }
   150  
   151  // downgrade downgrades a v1 pipeline.
   152  func (d *Downgrader) downgrade(src []*v1.Config) ([]byte, error) {
   153  	var buf bytes.Buffer
   154  	for i, p := range src {
   155  		config := new(v0.Config)
   156  
   157  		// TODO pipeline.name removed from spec
   158  
   159  		// use name from yaml if set and name not provided
   160  		// if p.Name != "" && d.pipelineId == "default" {
   161  		// 	config.Pipeline.ID = slug.Create(p.Name)
   162  		// 	config.Pipeline.Name = p.Name
   163  		// } else {
   164  		config.Pipeline.ID = d.pipelineId
   165  		config.Pipeline.Name = d.pipelineName
   166  		// }
   167  
   168  		config.Pipeline.Org = d.pipelineOrg
   169  		config.Pipeline.Project = d.pipelineProj
   170  		config.Pipeline.Props.CI.Codebase = v0.Codebase{
   171  			Name:  d.codebaseName,
   172  			Conn:  d.codebaseConn,
   173  			Build: "<+input>",
   174  		}
   175  		// FIXME: this is subject to a nil pointer
   176  		if p.Spec.(*v1.Pipeline).Options != nil {
   177  			config.Pipeline.Variables = convertVariables(p.Spec.(*v1.Pipeline).Options.Envs)
   178  		}
   179  
   180  		// convert stages
   181  		// FIXME: this is subject to a nil pointer
   182  		for _, stage := range p.Spec.(*v1.Pipeline).Stages {
   183  			// skip nil stages. this is un-necessary, we have
   184  			// this logic in place just to be safe.
   185  			if stage == nil {
   186  				continue
   187  			}
   188  
   189  			// skip stages that are not CI stages, for now
   190  			if _, ok := stage.Spec.(*v1.StageCI); !ok {
   191  				continue
   192  			}
   193  
   194  			// convert the stage and add to the list
   195  			config.Pipeline.Stages = append(config.Pipeline.Stages, &v0.Stages{
   196  				Stage: d.convertStage(stage),
   197  			})
   198  		}
   199  		out, err := yaml.Marshal(config)
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  		if i > 0 {
   204  			buf.WriteString("\n---\n")
   205  		}
   206  		buf.Write(out)
   207  	}
   208  	return buf.Bytes(), nil
   209  }
   210  
   211  // helper function converts a drone pipeline stage to a
   212  // harness stage.
   213  //
   214  // TODO env variables to vars (stage-level)
   215  // TODO delegate selectors
   216  // TODO tags
   217  // TODO when
   218  // TODO failure strategy
   219  // TODO volumes
   220  // TODO if no stage clone, use global clone, if exists
   221  func (d *Downgrader) convertStage(stage *v1.Stage) *v0.Stage {
   222  	// extract the spec from the v1 stage
   223  	spec := stage.Spec.(*v1.StageCI)
   224  
   225  	var steps []*v0.Steps
   226  	// convert each drone step to a harness step.
   227  	for _, v := range spec.Steps {
   228  		// the v0 yaml does not have the concept of
   229  		// a group step, so we append all steps in
   230  		// the group directly to the stage to emulate
   231  		// this behavior.
   232  		if _, ok := v.Spec.(*v1.StepGroup); ok {
   233  			steps = append(steps, d.convertStepGroup(v, 10)...)
   234  
   235  		} else {
   236  			// else convert the step and append to
   237  			// the stage.
   238  			if step := d.convertStep(v); step != nil {
   239  				steps = append(steps, step)
   240  			}
   241  		}
   242  	}
   243  
   244  	// enable clone by default
   245  	enableClone := true
   246  	if spec.Clone != nil && spec.Clone.Disabled == true {
   247  		enableClone = false
   248  	}
   249  
   250  	// convert volumes
   251  	if len(spec.Volumes) > 0 {
   252  		// TODO
   253  	}
   254  
   255  	//
   256  	// START TODO - refactor this into a helper function
   257  	//
   258  
   259  	var infra *v0.Infrastructure
   260  	var runtime *v0.Runtime
   261  
   262  	if spec.Runtime != nil {
   263  		// convert kubernetes
   264  		if kube, ok := spec.Runtime.Spec.(*v1.RuntimeKube); ok {
   265  			infra = &v0.Infrastructure{
   266  				Type: v0.InfraTypeKubernetesDirect,
   267  				Spec: &v0.InfraSpec{
   268  					Namespace: kube.Namespace,
   269  					Conn:      kube.Connector,
   270  				},
   271  			}
   272  			if infra.Spec.Namespace == "" {
   273  				kube.Namespace = d.kubeNamespace
   274  			}
   275  			if infra.Spec.Conn == "" {
   276  				kube.Connector = d.kubeConnector
   277  			}
   278  		}
   279  
   280  		// convert cloud
   281  		if _, ok := spec.Runtime.Spec.(*v1.RuntimeCloud); ok {
   282  			runtime = &v0.Runtime{
   283  				Type: "Cloud",
   284  				Spec: struct{}{},
   285  			}
   286  		}
   287  	}
   288  
   289  	// if neither cloud nor kubernetes are specified
   290  	// we default to cloud.
   291  	if runtime == nil && infra == nil {
   292  		runtime = &v0.Runtime{
   293  			Type: "Cloud",
   294  			Spec: struct{}{},
   295  		}
   296  	}
   297  
   298  	// if the user explicitly provides a kubernetes connector,
   299  	// we should override whatever was in the source yaml and
   300  	// force kubernetes.
   301  	if d.kubeConnector != "" {
   302  		runtime = nil
   303  		infra = &v0.Infrastructure{
   304  			Type: v0.InfraTypeKubernetesDirect,
   305  			Spec: &v0.InfraSpec{
   306  				Namespace: d.kubeNamespace,
   307  				Conn:      d.kubeConnector,
   308  			},
   309  		}
   310  	}
   311  
   312  	//
   313  	// END TODO
   314  	//
   315  
   316  	// convert the stage to a harness stage.
   317  	return &v0.Stage{
   318  		ID: d.identifiers.Generate(
   319  			slug.Create(stage.Id),
   320  			slug.Create(stage.Name),
   321  			slug.Create(stage.Type),
   322  		),
   323  		Name: convertName(stage.Name),
   324  		Type: v0.StageTypeCI,
   325  		Spec: v0.StageCI{
   326  			Cache:          convertCache(spec.Cache),
   327  			Clone:          enableClone,
   328  			Infrastructure: infra,
   329  			Platform:       convertPlatform(spec.Platform, runtime),
   330  			Runtime:        runtime,
   331  			Execution: v0.Execution{
   332  				Steps: steps,
   333  			},
   334  		},
   335  		When:     convertStageWhen(stage.When, ""),
   336  		Strategy: convertStrategy(stage.Strategy),
   337  		Vars:     convertVariables(spec.Envs),
   338  	}
   339  }
   340  
   341  // convertStrategy converts the v1.Strategy to the v0.Strategy
   342  func convertStrategy(v1Strategy *v1.Strategy) *v0.Strategy {
   343  	if v1Strategy == nil {
   344  		return nil
   345  	}
   346  	v0Strategy := v0.Strategy{}
   347  	switch v1Strategy.Type {
   348  	case "matrix":
   349  		v0Matrix := convertMatrix(v1Strategy.Spec.(*v1.Matrix))
   350  		v0Strategy.Matrix = v0Matrix
   351  	default:
   352  	}
   353  
   354  	return &v0Strategy
   355  }
   356  
   357  // convertMatrix converts the v1.Matrix to the v0.Matrix
   358  func convertMatrix(v1Matrix *v1.Matrix) map[string]interface{} {
   359  	matrix := make(map[string]interface{})
   360  
   361  	// Convert axis
   362  	for key, values := range v1Matrix.Axis {
   363  		matrix[key] = values
   364  	}
   365  
   366  	// Convert exclusions
   367  	var exclusions []v0.Exclusion
   368  	for _, v1Exclusion := range v1Matrix.Exclude {
   369  		exclusion := make(v0.Exclusion)
   370  		for key, values := range v1Exclusion {
   371  			exclusion[key] = values
   372  		}
   373  		exclusions = append(exclusions, exclusion)
   374  	}
   375  	matrix["exclude"] = exclusions
   376  
   377  	// Convert maxConcurrency
   378  	if v1Matrix.Concurrency != 0 {
   379  		matrix["maxConcurrency"] = v1Matrix.Concurrency
   380  	}
   381  
   382  	return matrix
   383  }
   384  
   385  // helper function converts a drone pipeline step to a
   386  // harness step.
   387  //
   388  // TODO unique identifier
   389  // TODO failure strategy
   390  // TODO matrix strategy
   391  // TODO when
   392  func (d *Downgrader) convertStep(src *v1.Step) *v0.Steps {
   393  	switch src.Spec.(type) {
   394  	case *v1.StepExec:
   395  		return &v0.Steps{Step: d.convertStepRun(src)}
   396  	case *v1.StepPlugin:
   397  		return &v0.Steps{Step: d.convertStepPlugin(src)}
   398  	case *v1.StepAction:
   399  		return &v0.Steps{Step: d.convertStepAction(src)}
   400  	case *v1.StepBitrise:
   401  		return &v0.Steps{Step: d.convertStepBitrise(src)}
   402  	case *v1.StepParallel:
   403  		return &v0.Steps{Parallel: d.convertStepParallel(src)}
   404  	case *v1.StepBackground:
   405  		return &v0.Steps{Step: d.convertStepBackground(src)}
   406  	default:
   407  		return nil // should not happen
   408  	}
   409  }
   410  
   411  // helper function to convert a Group step from the v1
   412  // structure to a list of steps. The v0 yaml does not have
   413  // an equivalent to the group step.
   414  func (d *Downgrader) convertStepGroup(src *v1.Step, depth int) []*v0.Steps {
   415  	if depth > MaxDepth {
   416  		return nil // Reached maximum depth. Stop recursion to prevent stack overflow
   417  	}
   418  	spec_ := src.Spec.(*v1.StepGroup)
   419  
   420  	var steps []*v0.Steps
   421  	for _, step := range spec_.Steps {
   422  		// If this step is a step group, recursively convert it
   423  		if _, ok := step.Spec.(*v1.StepGroup); ok {
   424  			steps = append(steps, d.convertStepGroup(step, depth+1)...)
   425  		} else {
   426  			// Else, convert the step
   427  			if dst := d.convertStep(step); dst != nil {
   428  				steps = append(steps, &v0.Steps{Step: dst.Step})
   429  			}
   430  		}
   431  	}
   432  	return steps
   433  }
   434  
   435  // helper function to convert a Parallel step from the v1
   436  // structure to the v0 harness structure.
   437  func (d *Downgrader) convertStepParallel(src *v1.Step) []*v0.Steps {
   438  	spec_ := src.Spec.(*v1.StepParallel)
   439  
   440  	var steps []*v0.Steps
   441  	for _, step := range spec_.Steps {
   442  		if dst := d.convertStep(step); dst != nil {
   443  			steps = append(steps, &v0.Steps{Step: dst.Step})
   444  		}
   445  	}
   446  	return steps
   447  }
   448  
   449  // helper function to convert a Run step from the v1
   450  // structure to the v0 harness structure.
   451  //
   452  // TODO convert outputs
   453  // TODO convert resources
   454  // TODO convert reports
   455  func (d *Downgrader) convertStepRun(src *v1.Step) *v0.Step {
   456  	spec_ := src.Spec.(*v1.StepExec)
   457  	var id = d.identifiers.Generate(
   458  		slug.Create(src.Id),
   459  		slug.Create(src.Name),
   460  		slug.Create(src.Type))
   461  	if src.Name == "" {
   462  		src.Name = id
   463  	}
   464  	return &v0.Step{
   465  		ID:      id,
   466  		Name:    convertName(src.Name),
   467  		Type:    v0.StepTypeRun,
   468  		Timeout: convertTimeout(src.Timeout),
   469  		Spec: &v0.StepRun{
   470  			Env:             spec_.Envs,
   471  			Command:         spec_.Run,
   472  			ConnRef:         d.dockerhubConn,
   473  			Image:           spec_.Image,
   474  			ImagePullPolicy: convertImagePull(spec_.Pull),
   475  			Privileged:      spec_.Privileged,
   476  			RunAsUser:       spec_.User,
   477  			Reports:         convertReports(spec_.Reports),
   478  			Shell:           strings.Title(spec_.Shell),
   479  		},
   480  		When:     convertStepWhen(src.When, id),
   481  		Strategy: convertStrategy(src.Strategy),
   482  	}
   483  }
   484  
   485  // helper function to convert reports from the v1 to v0
   486  func convertReports(reports []*v1.Report) *v0.Report {
   487  	if reports == nil || len(reports) == 0 {
   488  		return nil
   489  	}
   490  
   491  	// Initialize an empty slice to store all paths
   492  	allPaths := []string{}
   493  
   494  	// Loop over reports and collect all paths
   495  	for _, report := range reports {
   496  		allPaths = append(allPaths, report.Path...)
   497  	}
   498  
   499  	reportJunit := v0.ReportJunit{
   500  		Paths: allPaths,
   501  	}
   502  
   503  	v0Report := v0.Report{
   504  		// Assuming all reports have the same type
   505  		Type: "JUnit",
   506  		Spec: &reportJunit,
   507  	}
   508  
   509  	return &v0Report
   510  }
   511  
   512  // helper function to convert a Bitrise step from the v1
   513  // structure to the v0 harness structure.
   514  //
   515  // TODO convert resources
   516  func (d *Downgrader) convertStepBackground(src *v1.Step) *v0.Step {
   517  	spec_ := src.Spec.(*v1.StepBackground)
   518  	var id = d.identifiers.Generate(
   519  		slug.Create(src.Id),
   520  		slug.Create(src.Name),
   521  		slug.Create(src.Type))
   522  	if src.Name == "" {
   523  		src.Name = id
   524  	}
   525  	// convert the entrypoint string to a slice.
   526  	var entypoint []string
   527  	if spec_.Entrypoint != "" {
   528  		entypoint = []string{spec_.Entrypoint}
   529  	}
   530  	return &v0.Step{
   531  		ID:   id,
   532  		Name: convertName(src.Name),
   533  		Type: v0.StepTypeBackground,
   534  		Spec: &v0.StepBackground{
   535  			Command:         spec_.Run,
   536  			ConnRef:         d.dockerhubConn,
   537  			Entrypoint:      entypoint,
   538  			Env:             spec_.Envs,
   539  			Image:           spec_.Image,
   540  			ImagePullPolicy: convertImagePull(spec_.Pull),
   541  			Privileged:      spec_.Privileged,
   542  			RunAsUser:       spec_.User,
   543  			PortBindings:    convertPorts(spec_.Ports),
   544  		},
   545  		When: convertStepWhen(src.When, id),
   546  	}
   547  }
   548  
   549  // helper function to convert a Plugin step from the v1
   550  // structure to the v0 harness structure.
   551  //
   552  // TODO convert resources
   553  // TODO convert reports
   554  func (d *Downgrader) convertStepPlugin(src *v1.Step) *v0.Step {
   555  	spec_ := src.Spec.(*v1.StepPlugin)
   556  
   557  	if strings.Contains(spec_.Image, "kaniko-docker") {
   558  		return d.convertStepPluginToDocker(src)
   559  	}
   560  
   561  	var id = d.identifiers.Generate(
   562  		slug.Create(src.Id),
   563  		slug.Create(src.Name),
   564  		slug.Create(src.Type))
   565  	if src.Name == "" {
   566  		src.Name = id
   567  	}
   568  	return &v0.Step{
   569  		ID:      id,
   570  		Name:    src.Name,
   571  		Type:    v0.StepTypePlugin,
   572  		Timeout: convertTimeout(src.Timeout),
   573  		Spec: &v0.StepPlugin{
   574  			Env:             spec_.Envs,
   575  			ConnRef:         d.dockerhubConn,
   576  			Image:           spec_.Image,
   577  			ImagePullPolicy: convertImagePull(spec_.Pull),
   578  			Settings:        convertSettings(spec_.With),
   579  			Privileged:      spec_.Privileged,
   580  			RunAsUser:       spec_.User,
   581  		},
   582  		When: convertStepWhen(src.When, id),
   583  	}
   584  }
   585  
   586  // helper function to convert a Plugin step from the v1
   587  // structure to the v0 harness structure.
   588  //
   589  // TODO convert resources
   590  // TODO convert reports
   591  func (d *Downgrader) convertStepPluginToDocker(src *v1.Step) *v0.Step {
   592  	spec_ := src.Spec.(*v1.StepPlugin)
   593  	var id = d.identifiers.Generate(
   594  		slug.Create(src.Id),
   595  		slug.Create(src.Name),
   596  		slug.Create(src.Type))
   597  	if src.Name == "" {
   598  		src.Name = id
   599  	}
   600  
   601  	stepDocker := &v0.StepDocker{
   602  		Caching:      true,
   603  		ConnectorRef: "<+input>",
   604  		Privileged:   spec_.Privileged,
   605  		RunAsUser:    spec_.User,
   606  	}
   607  
   608  	// Check if "repo" exists in spec_.With
   609  	if repo, ok := spec_.With["repo"].(string); ok {
   610  		// If "repo" exists, set it in StepDocker
   611  		stepDocker.Repo = repo
   612  	}
   613  	if context, ok := spec_.With["context"].(string); ok {
   614  		// If "context" exists, set it in StepDocker
   615  		stepDocker.Context = context
   616  	}
   617  	if dockerfile, ok := spec_.With["dockerfile"].(string); ok {
   618  		// If "dockerfile" exists, set it in StepDocker
   619  		stepDocker.Dockerfile = dockerfile
   620  	}
   621  	if target, ok := spec_.With["target"].(string); ok {
   622  		// If "target" exists, set it in StepDocker
   623  		stepDocker.Target = target
   624  	}
   625  	// if cacheRepo, ok := spec_.With["cache_repo"].(string); ok {
   626  	// 	// If "cache_repo" exists, set it in StepDocker
   627  	// 	stepDocker.RemoteCacheRepo = cacheRepo
   628  	// }
   629  	if tagsInterface, ok := spec_.With["tags"]; ok {
   630  		stepDocker.Tags = extractStringSlice(tagsInterface)
   631  	}
   632  	if buildArgsInterface, ok := spec_.With["build_args"]; ok {
   633  		stepDocker.BuildsArgs = extractStringMap(buildArgsInterface)
   634  	}
   635  	if labelsInterface, ok := spec_.With["custom_labels"]; ok {
   636  		stepDocker.Labels = extractStringMap(labelsInterface)
   637  	}
   638  
   639  	return &v0.Step{
   640  		ID:      id,
   641  		Name:    src.Name,
   642  		Type:    v0.StepTypeBuildAndPushDockerRegistry,
   643  		Timeout: convertTimeout(src.Timeout),
   644  		Spec:    stepDocker,
   645  		When:    convertStepWhen(src.When, id),
   646  	}
   647  }
   648  
   649  // helper function to convert an Action step from the v1
   650  // structure to the v0 harness structure.
   651  func (d *Downgrader) convertStepAction(src *v1.Step) *v0.Step {
   652  	spec_ := src.Spec.(*v1.StepAction)
   653  	var id = d.identifiers.Generate(
   654  		slug.Create(src.Id),
   655  		slug.Create(src.Name),
   656  		slug.Create(src.Type))
   657  	if src.Name == "" {
   658  		src.Name = id
   659  	}
   660  	return &v0.Step{
   661  		ID:      id,
   662  		Name:    convertName(src.Name),
   663  		Type:    v0.StepTypeAction,
   664  		Timeout: convertTimeout(src.Timeout),
   665  		Spec: &v0.StepAction{
   666  			Uses: spec_.Uses,
   667  			With: convertSettings(spec_.With),
   668  			Envs: spec_.Envs,
   669  		},
   670  		When: convertStepWhen(src.When, id),
   671  	}
   672  }
   673  
   674  // helper function to convert a Bitrise step from the v1
   675  // structure to the v0 harness structure.
   676  func (d *Downgrader) convertStepBitrise(src *v1.Step) *v0.Step {
   677  	spec_ := src.Spec.(*v1.StepBitrise)
   678  	var id = d.identifiers.Generate(
   679  		slug.Create(src.Id),
   680  		slug.Create(src.Name),
   681  		slug.Create(src.Type))
   682  	if src.Name == "" {
   683  		src.Name = id
   684  	}
   685  	return &v0.Step{
   686  		ID:      id,
   687  		Name:    convertName(src.Name),
   688  		Type:    v0.StepTypeBitrise,
   689  		Timeout: convertTimeout(src.Timeout),
   690  		Spec: &v0.StepBitrise{
   691  			Uses: spec_.Uses,
   692  			With: convertSettings(spec_.With),
   693  			Envs: spec_.Envs,
   694  		},
   695  		When: convertStepWhen(src.When, id),
   696  	}
   697  }
   698  
   699  func convertPorts(ports []string) map[string]string {
   700  	if len(ports) == 0 {
   701  		return nil
   702  	}
   703  	bindings := make(map[string]string, len(ports))
   704  	for _, port := range ports {
   705  		split := strings.Split(port, ":")
   706  		if len(split) == 1 {
   707  			bindings[split[0]] = split[0]
   708  		} else if len(split) == 2 {
   709  			bindings[split[0]] = split[1]
   710  		}
   711  	}
   712  	return bindings
   713  }
   714  
   715  func convertCache(src *v1.Cache) *v0.Cache {
   716  	if src == nil {
   717  		return nil
   718  	}
   719  	return &v0.Cache{
   720  		Enabled: src.Enabled,
   721  		Key:     src.Key,
   722  		Paths:   src.Paths,
   723  	}
   724  }
   725  
   726  func convertVariables(src map[string]string) []*v0.Variable {
   727  	if src == nil {
   728  		return nil
   729  	}
   730  	var vars []*v0.Variable
   731  	var keys []string
   732  	for k := range src {
   733  		keys = append(keys, k)
   734  	}
   735  	sort.Strings(keys)
   736  	for _, k := range keys {
   737  		v := src[k]
   738  		vars = append(vars, &v0.Variable{
   739  			Name:  k,
   740  			Value: v,
   741  			Type:  "String",
   742  		})
   743  	}
   744  	return vars
   745  }
   746  
   747  func convertSettings(src map[string]interface{}) map[string]interface{} {
   748  	dst := map[string]interface{}{}
   749  	for k, v := range src {
   750  		switch v := v.(type) {
   751  		case []interface{}:
   752  			var strList []string
   753  			for _, item := range v {
   754  				strList = append(strList, fmt.Sprint(item))
   755  			}
   756  			dst[k] = strList
   757  		default:
   758  			dst[k] = fmt.Sprint(v)
   759  		}
   760  	}
   761  	return dst
   762  }
   763  
   764  func convertTimeout(s string) v0.Duration {
   765  	d, _ := time.ParseDuration(s)
   766  	return v0.Duration{
   767  		Duration: d,
   768  	}
   769  }
   770  
   771  func convertImagePull(v string) (s string) {
   772  	switch v {
   773  	case "always":
   774  		return v0.ImagePullAlways
   775  	case "never":
   776  		return v0.ImagePullNever
   777  	case "if-not-exists":
   778  		return v0.ImagePullIfNotPresent
   779  	default:
   780  		return ""
   781  	}
   782  }
   783  
   784  func convertPlatform(platform *v1.Platform, runtime *v0.Runtime) *v0.Platform {
   785  	if platform != nil {
   786  		var os, arch string
   787  
   788  		// convert the OS name
   789  		switch platform.Os {
   790  		case "linux":
   791  			os = "Linux"
   792  		case "windows":
   793  			os = "Windows"
   794  		case "macos", "mac", "darwin":
   795  			os = "MacOS"
   796  		default:
   797  			os = "Linux"
   798  		}
   799  
   800  		// convert the Arch name
   801  		switch platform.Arch {
   802  		case "amd64":
   803  			arch = "Amd64"
   804  		case "arm", "arm64":
   805  			arch = "Arm64"
   806  		default:
   807  			// choose the default architecture
   808  			// based on the os.
   809  			switch os {
   810  			case "MacOS":
   811  				arch = "Arm64"
   812  			default:
   813  				arch = "Amd64"
   814  			}
   815  		}
   816  
   817  		// ensure supported infra when using harness cloud
   818  		if runtime != nil && runtime.Type == "Cloud" {
   819  			switch os {
   820  			case "MacOS":
   821  				// force amd64 for Mac when using Cloud
   822  				arch = "Arm64"
   823  			case "Windows":
   824  				// force amd64 for Windows when using Cloud
   825  				arch = "Amd64"
   826  			}
   827  		}
   828  
   829  		return &v0.Platform{
   830  			OS:   os,
   831  			Arch: arch,
   832  		}
   833  	} else {
   834  		// default to linux amd64
   835  		return &v0.Platform{
   836  			OS:   "Linux",
   837  			Arch: "Amd64",
   838  		}
   839  	}
   840  }
   841  
   842  func convertStepWhen(when *v1.When, stepId string) *v0.StepWhen {
   843  	if when == nil {
   844  		return nil
   845  	}
   846  
   847  	newWhen := &v0.StepWhen{
   848  		StageStatus: "Success", // default
   849  	}
   850  	var conditions []string
   851  
   852  	for _, cond := range when.Cond {
   853  		for k, v := range cond {
   854  			switch k {
   855  			case "event":
   856  				if v.In != nil {
   857  					var eventConditions []string
   858  					for _, event := range v.In {
   859  						eventMapping, ok := eventMap[event]
   860  						if !ok {
   861  							continue
   862  						}
   863  						eventConditions = append(eventConditions, fmt.Sprintf("%s %s %q", eventMapping["jexl"], eventMapping["operator"], eventMapping["event"]))
   864  					}
   865  					if len(eventConditions) > 0 {
   866  						conditions = append(conditions, fmt.Sprintf("(%s)", strings.Join(eventConditions, " || ")))
   867  					}
   868  				}
   869  				if v.Not != nil && v.Not.In != nil {
   870  					var notEventConditions []string
   871  					for _, event := range v.Not.In {
   872  						eventMapping, ok := eventMap[event]
   873  						if !ok {
   874  							continue
   875  						}
   876  						notEventConditions = append(notEventConditions, fmt.Sprintf("%s %s %q", eventMapping["jexl"], eventMapping["inverseOperator"], eventMapping["event"]))
   877  					}
   878  					if len(notEventConditions) > 0 {
   879  						conditions = append(conditions, fmt.Sprintf("(%s)", strings.Join(notEventConditions, " && ")))
   880  					}
   881  				}
   882  			case "status":
   883  				if v.Eq != "" {
   884  					newWhen.StageStatus = v.Eq
   885  				}
   886  				if v.In != nil {
   887  					var statusConditions []string
   888  					for _, status := range v.In {
   889  						statusConditions = append(statusConditions, fmt.Sprintf("<+execution.steps.%s.status> == %q", stepId, status))
   890  					}
   891  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(statusConditions, " || ")))
   892  				}
   893  			case "branch":
   894  				if v.In != nil {
   895  					var branchConditions []string
   896  					for _, branch := range v.In {
   897  						branchConditions = append(branchConditions, fmt.Sprintf("<+trigger.targetBranch> == %q", branch))
   898  					}
   899  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(branchConditions, " || ")))
   900  				}
   901  				if v.Not != nil && v.Not.In != nil {
   902  					var notBranchConditions []string
   903  					for _, branch := range v.Not.In {
   904  						notBranchConditions = append(notBranchConditions, fmt.Sprintf("<+trigger.targetBranch> != %q", branch))
   905  					}
   906  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(notBranchConditions, " && ")))
   907  				}
   908  			case "repo":
   909  				if v.In != nil {
   910  					var repoConditions []string
   911  					for _, repo := range v.In {
   912  						repoConditions = append(repoConditions, fmt.Sprintf("<+trigger.payload.repository.name> == %q", repo))
   913  					}
   914  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(repoConditions, " || ")))
   915  				}
   916  				if v.Not != nil && v.Not.In != nil {
   917  					var notRepoConditions []string
   918  					for _, repo := range v.Not.In {
   919  						notRepoConditions = append(notRepoConditions, fmt.Sprintf("<+trigger.payload.repository.name> != %q", repo))
   920  					}
   921  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(notRepoConditions, " && ")))
   922  				}
   923  			case "ref":
   924  				if v.In != nil {
   925  					var refConditions []string
   926  					for _, ref := range v.In {
   927  						refConditions = append(refConditions, fmt.Sprintf("<+trigger.payload.ref> == %q", ref))
   928  					}
   929  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(refConditions, " || ")))
   930  				}
   931  				if v.Not != nil && v.Not.In != nil {
   932  					var notRefConditions []string
   933  					for _, ref := range v.Not.In {
   934  						notRefConditions = append(notRefConditions, fmt.Sprintf("<+trigger.payload.ref> != %q", ref))
   935  					}
   936  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(notRefConditions, " && ")))
   937  				}
   938  			}
   939  		}
   940  	}
   941  
   942  	if len(conditions) > 0 {
   943  		newWhen.Condition = strings.Join(conditions, " && ")
   944  	}
   945  
   946  	return newWhen
   947  }
   948  
   949  func convertStageWhen(when *v1.When, stepId string) *v0.StageWhen {
   950  	if when == nil {
   951  		return nil
   952  	}
   953  
   954  	newWhen := &v0.StageWhen{
   955  		PipelineStatus: "Success", // default
   956  	}
   957  	var conditions []string
   958  
   959  	for _, cond := range when.Cond {
   960  		for k, v := range cond {
   961  			switch k {
   962  			case "event":
   963  				if v.In != nil {
   964  					var eventConditions []string
   965  					for _, event := range v.In {
   966  						eventMapping, ok := eventMap[event]
   967  						if !ok {
   968  							continue
   969  						}
   970  						eventConditions = append(eventConditions, fmt.Sprintf("%s %s %q", eventMapping["jexl"], eventMapping["operator"], eventMapping["event"]))
   971  					}
   972  					if len(eventConditions) > 0 {
   973  						conditions = append(conditions, fmt.Sprintf("(%s)", strings.Join(eventConditions, " || ")))
   974  					}
   975  				}
   976  				if v.Not != nil && v.Not.In != nil {
   977  					var notEventConditions []string
   978  					for _, event := range v.Not.In {
   979  						eventMapping, ok := eventMap[event]
   980  						if !ok {
   981  							continue
   982  						}
   983  						notEventConditions = append(notEventConditions, fmt.Sprintf("%s %s %q", eventMapping["jexl"], eventMapping["inverseOperator"], eventMapping["event"]))
   984  					}
   985  					if len(notEventConditions) > 0 {
   986  						conditions = append(conditions, fmt.Sprintf("(%s)", strings.Join(notEventConditions, " && ")))
   987  					}
   988  				}
   989  			case "status":
   990  				if v.Eq != "" {
   991  					newWhen.PipelineStatus = v.Eq
   992  				}
   993  				if v.In != nil {
   994  					var statusConditions []string
   995  					for _, status := range v.In {
   996  						statusConditions = append(statusConditions, fmt.Sprintf("<+execution.steps.%s.status> == %q", stepId, status))
   997  					}
   998  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(statusConditions, " || ")))
   999  				}
  1000  			case "branch":
  1001  				if v.In != nil {
  1002  					var branchConditions []string
  1003  					for _, branch := range v.In {
  1004  						branchConditions = append(branchConditions, fmt.Sprintf("<+trigger.targetBranch> == %q", branch))
  1005  					}
  1006  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(branchConditions, " || ")))
  1007  				}
  1008  				if v.Not != nil && v.Not.In != nil {
  1009  					var notBranchConditions []string
  1010  					for _, branch := range v.Not.In {
  1011  						notBranchConditions = append(notBranchConditions, fmt.Sprintf("<+trigger.targetBranch> != %q", branch))
  1012  					}
  1013  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(notBranchConditions, " && ")))
  1014  				}
  1015  			case "repo":
  1016  				if v.In != nil {
  1017  					var repoConditions []string
  1018  					for _, repo := range v.In {
  1019  						repoConditions = append(repoConditions, fmt.Sprintf("<+trigger.payload.repository.name> == %q", repo))
  1020  					}
  1021  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(repoConditions, " || ")))
  1022  				}
  1023  				if v.Not != nil && v.Not.In != nil {
  1024  					var notRepoConditions []string
  1025  					for _, repo := range v.Not.In {
  1026  						notRepoConditions = append(notRepoConditions, fmt.Sprintf("<+trigger.payload.repository.name> != %q", repo))
  1027  					}
  1028  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(notRepoConditions, " && ")))
  1029  				}
  1030  			case "ref":
  1031  				if v.In != nil {
  1032  					var refConditions []string
  1033  					for _, ref := range v.In {
  1034  						refConditions = append(refConditions, fmt.Sprintf("<+trigger.payload.ref> == %q", ref))
  1035  					}
  1036  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(refConditions, " || ")))
  1037  				}
  1038  				if v.Not != nil && v.Not.In != nil {
  1039  					var notRefConditions []string
  1040  					for _, ref := range v.Not.In {
  1041  						notRefConditions = append(notRefConditions, fmt.Sprintf("<+trigger.payload.ref> != %q", ref))
  1042  					}
  1043  					conditions = append(conditions, fmt.Sprintf("%s", strings.Join(notRefConditions, " && ")))
  1044  				}
  1045  			}
  1046  		}
  1047  	}
  1048  
  1049  	if len(conditions) > 0 {
  1050  		newWhen.Condition = strings.Join(conditions, " && ")
  1051  	}
  1052  
  1053  	return newWhen
  1054  }
  1055  
  1056  func extractStringSlice(input interface{}) []string {
  1057  	switch data := input.(type) {
  1058  	case []interface{}:
  1059  		var result []string
  1060  		for _, item := range data {
  1061  			result = append(result, strings.SplitN(fmt.Sprintf("%v", item), ",", -1)...)
  1062  		}
  1063  		return result
  1064  	case interface{}:
  1065  		return []string{fmt.Sprintf("%v", data)}
  1066  	default:
  1067  		return nil
  1068  	}
  1069  }
  1070  
  1071  func extractStringMap(input interface{}) map[string]string {
  1072  	switch data := input.(type) {
  1073  	case []interface{}:
  1074  		result := make(map[string]string)
  1075  		for _, item := range data {
  1076  			for _, keyValue := range strings.SplitN(fmt.Sprintf("%v", item), ",", -1) {
  1077  				pair := strings.SplitN(keyValue, "=", 2)
  1078  				if len(pair) == 2 {
  1079  					result[pair[0]] = pair[1]
  1080  				}
  1081  			}
  1082  		}
  1083  		return result
  1084  	case interface{}:
  1085  		result := make(map[string]string)
  1086  		for _, keyValue := range strings.SplitN(fmt.Sprintf("%v", data), ",", -1) {
  1087  			pair := strings.SplitN(keyValue, "=", 2)
  1088  			if len(pair) == 2 {
  1089  				result[pair[0]] = pair[1]
  1090  			}
  1091  		}
  1092  		return result
  1093  	default:
  1094  		return nil
  1095  	}
  1096  }