github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/cloudbuild/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 cloudbuild converts Google Cloud Build pipelines to Harness pipelines.
    16  package cloudbuild
    17  
    18  import (
    19  	"bytes"
    20  	"io"
    21  	"os"
    22  	"path"
    23  	"strings"
    24  	"time"
    25  
    26  	cloudbuild "github.com/drone/go-convert/convert/cloudbuild/yaml"
    27  	"github.com/drone/go-convert/internal/store"
    28  	harness "github.com/drone/spec/dist/go"
    29  
    30  	"github.com/ghodss/yaml"
    31  )
    32  
    33  // Converter converts a Cloud Build pipeline to a Harness
    34  // v1 pipeline.
    35  type Converter struct {
    36  	kubeEnabled   bool
    37  	kubeNamespace string
    38  	kubeConnector string
    39  	dockerhubConn string
    40  	identifiers   *store.Identifiers
    41  }
    42  
    43  // New creates a new Converter that converts a Cloud Build
    44  // pipeline to a Harness v1 pipeline.
    45  func New(options ...Option) *Converter {
    46  	d := new(Converter)
    47  
    48  	// create the unique identifier store. this store
    49  	// is used for registering unique identifiers to
    50  	// prevent duplicate names, unique index violations.
    51  	d.identifiers = store.New()
    52  
    53  	// loop through and apply the options.
    54  	for _, option := range options {
    55  		option(d)
    56  	}
    57  
    58  	// set the default kubernetes namespace.
    59  	if d.kubeNamespace == "" {
    60  		d.kubeNamespace = "default"
    61  	}
    62  
    63  	// set the runtime to kubernetes if the kubernetes
    64  	// connector is configured.
    65  	if d.kubeConnector != "" {
    66  		d.kubeEnabled = true
    67  	}
    68  
    69  	return d
    70  }
    71  
    72  // Convert downgrades a v1 pipeline.
    73  func (d *Converter) Convert(r io.Reader) ([]byte, error) {
    74  	src, err := cloudbuild.Parse(r)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	return d.convert(src)
    79  }
    80  
    81  // ConvertString downgrades a v1 pipeline.
    82  func (d *Converter) ConvertBytes(b []byte) ([]byte, error) {
    83  	return d.Convert(
    84  		bytes.NewBuffer(b),
    85  	)
    86  }
    87  
    88  // ConvertString downgrades a v1 pipeline.
    89  func (d *Converter) ConvertString(s string) ([]byte, error) {
    90  	return d.Convert(
    91  		bytes.NewBufferString(s),
    92  	)
    93  }
    94  
    95  // ConvertFile downgrades a v1 pipeline.
    96  func (d *Converter) ConvertFile(p string) ([]byte, error) {
    97  	f, err := os.Open(p)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	defer f.Close()
   102  	return d.Convert(f)
   103  }
   104  
   105  // converts converts a Cloud Build pipeline to a Harness pipeline.
   106  func (d *Converter) convert(src *cloudbuild.Config) ([]byte, error) {
   107  
   108  	// create the harness pipeline spec
   109  	pipeline := &harness.Pipeline{
   110  		Options: new(harness.Default),
   111  	}
   112  
   113  	// create the harness pipeline
   114  	config := &harness.Config{
   115  		Version: 1,
   116  		Kind:    "pipeline",
   117  		Spec:    pipeline,
   118  	}
   119  
   120  	// convert subsitutions to inputs
   121  	if v := src.Substitutions; len(v) != 0 {
   122  		pipeline.Inputs = map[string]*harness.Input{}
   123  		for key, val := range src.Substitutions {
   124  			pipeline.Inputs[key] = &harness.Input{
   125  				Type:    "string",
   126  				Default: val,
   127  			}
   128  		}
   129  	}
   130  
   131  	// convert pipeline timeout
   132  	if v := src.Timeout; v != 0 {
   133  		pipeline.Options.Timeout = convertTimeout(v)
   134  	}
   135  
   136  	spec := &harness.StageCI{
   137  		Cache: nil, // No Google equivalent
   138  		Envs:  nil,
   139  		Platform: &harness.Platform{
   140  			Os:   harness.OSLinux.String(),
   141  			Arch: harness.ArchAmd64.String(),
   142  		},
   143  		Runtime: d.convertRuntime(src),
   144  		Steps:   d.convertSteps(src),
   145  	}
   146  
   147  	// add global environment variables
   148  	uniqueVols := map[string]struct{}{}
   149  	if opts := src.Options; opts != nil {
   150  		spec.Envs = convertEnv(opts.Env)
   151  
   152  		// add global volumes
   153  		if vols := opts.Volumes; len(vols) > 0 {
   154  			for _, vol := range vols {
   155  				uniqueVols[vol.Name] = struct{}{}
   156  				spec.Volumes = append(spec.Volumes, &harness.Volume{
   157  					Name: vol.Name,
   158  					Type: "temp",
   159  					Spec: &harness.VolumeTemp{},
   160  				})
   161  			}
   162  		}
   163  	}
   164  	// add step volumes
   165  	for _, step := range src.Steps {
   166  		for _, vol := range step.Volumes {
   167  			// do not add the volume if already exists
   168  			if _, ok := uniqueVols[vol.Name]; ok {
   169  				continue
   170  			}
   171  			uniqueVols[vol.Name] = struct{}{}
   172  			spec.Volumes = append(spec.Volumes, &harness.Volume{
   173  				Name: vol.Name,
   174  				Type: "temp",
   175  				Spec: &harness.VolumeTemp{},
   176  			})
   177  		}
   178  	}
   179  
   180  	if d.kubeEnabled {
   181  		spec.Volumes = append(spec.Volumes, &harness.Volume{
   182  			Name: "dockersock",
   183  			Type: "temp",
   184  			Spec: &harness.VolumeTemp{},
   185  		})
   186  	} else {
   187  		spec.Volumes = append(spec.Volumes, &harness.Volume{
   188  			Name: "dockersock",
   189  			Type: "host",
   190  			Spec: &harness.VolumeHost{
   191  				Path: "/var/run/docker.sock",
   192  			},
   193  		})
   194  	}
   195  
   196  	// TODO src.Secrets
   197  	// TODO src.Availablesecrets
   198  	// TODO opts.Secretenv
   199  
   200  	// append steps to publish artifacts
   201  	if v := src.Artifacts; v != nil {
   202  		// TODO
   203  		// https://cloud.google.com/build/docs/build-config-file-schema#artifacts
   204  		// https://cloud.google.com/build/docs/build-config-file-schema#mavenartifacts
   205  		// https://cloud.google.com/build/docs/build-config-file-schema#pythonpackages
   206  	}
   207  
   208  	// append steps to push docker images
   209  	if v := src.Images; len(v) != 0 {
   210  		// TODO
   211  		// https://cloud.google.com/build/docs/build-config-file-schema#images
   212  	}
   213  
   214  	// conver pipeilne stages
   215  	pipeline.Stages = append(pipeline.Stages, &harness.Stage{
   216  		Name:     "pipeline",
   217  		Desc:     "converted from google cloud build",
   218  		Type:     "ci",
   219  		Delegate: nil, // No Google equivalent
   220  		Failure:  nil, // No Google equivalent
   221  		When:     nil, // No Google equivalent
   222  		Spec:     spec,
   223  	})
   224  
   225  	// replace google cloud build substitution variable
   226  	// with harness jexl expressions
   227  	config, err := replaceAll(
   228  		config,
   229  		combineEnv(
   230  			envMappingJexl,
   231  			mapInputsToExpr(src.Substitutions),
   232  		),
   233  	)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	// map cloud build environment variables to harness
   239  	// environment variables using jexl.
   240  	config.Spec.(*harness.Pipeline).Options.Envs = envMappingJexl
   241  
   242  	// marshal the harness yaml
   243  	out, err := yaml.Marshal(config)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	return out, nil
   249  }
   250  
   251  func (d *Converter) convertRuntime(src *cloudbuild.Config) *harness.Runtime {
   252  	if d.kubeEnabled {
   253  		return &harness.Runtime{
   254  			Type: "kubernetes",
   255  			Spec: &harness.RuntimeKube{
   256  				Namespace: d.kubeNamespace,
   257  				Connector: d.kubeConnector,
   258  			},
   259  		}
   260  	}
   261  	spec := new(harness.RuntimeCloud)
   262  	if src.Options != nil {
   263  		spec.Size = convertMachine(src.Options.Machinetype)
   264  	}
   265  	return &harness.Runtime{
   266  		Type: "cloud",
   267  		Spec: spec,
   268  	}
   269  }
   270  
   271  func (d *Converter) convertSteps(src *cloudbuild.Config) []*harness.Step {
   272  	var steps []*harness.Step
   273  	for _, step := range src.Steps {
   274  		// skip git clone steps by default
   275  		if strings.HasPrefix(step.Name, "gcr.io/cloud-builders/git") {
   276  			continue
   277  		}
   278  		steps = append(steps, d.convertStep(src, step))
   279  	}
   280  	return steps
   281  }
   282  
   283  func (d *Converter) convertStep(src *cloudbuild.Config, srcstep *cloudbuild.Step) *harness.Step {
   284  
   285  	return &harness.Step{
   286  		Name: d.identifiers.Generate(
   287  			srcstep.ID,
   288  			// fallback to the last sebment of the container
   289  			// name and use as the base name.
   290  			path.Base(srcstep.Name),
   291  		),
   292  		Desc:    "",  // No Google equivalent
   293  		When:    nil, // No Google equivalent
   294  		Failure: createFailurestrategy(srcstep),
   295  		Type:    "script",
   296  		Timeout: convertTimeout(srcstep.Timeout),
   297  		Spec: &harness.StepExec{
   298  			Image:      srcstep.Name,
   299  			Connector:  d.dockerhubConn,
   300  			Privileged: false, // No Google Equivalent
   301  			Pull:       "",    // No Google equivalent
   302  			Shell:      "",    // No Google equivalent
   303  			User:       "",    // No Google equivalent
   304  			Group:      "",    // No Google equivalent
   305  			Network:    "",    // No Google equivalent
   306  			Entrypoint: srcstep.Entrypoint,
   307  			Args:       srcstep.Args,
   308  			Run:        srcstep.Script,
   309  			Envs:       convertEnv(srcstep.Env),
   310  			Resources:  nil, // No Google equivalent
   311  			Reports:    nil, // No Google equivalent
   312  			Mount:      createMounts(src, srcstep),
   313  
   314  			// TODO support step.dir
   315  			// TODO support step.secretEnv
   316  		},
   317  	}
   318  }
   319  
   320  func createFailurestrategy(src *cloudbuild.Step) *harness.FailureList {
   321  	if src.Allowfailure == false && len(src.Allowexitcodes) == 0 {
   322  		return nil
   323  	}
   324  	return &harness.FailureList{
   325  		Items: []*harness.Failure{
   326  			{
   327  				Errors: []string{"all"},
   328  				Action: &harness.FailureAction{
   329  					Type: "ignore",
   330  					Spec: &harness.Ignore{},
   331  					// TODO exit_codes needs to be re-added to spec
   332  					// ExitCodes: src.Allowexitcodes,
   333  				},
   334  			},
   335  		},
   336  	}
   337  }
   338  
   339  func createMounts(src *cloudbuild.Config, srcstep *cloudbuild.Step) []*harness.Mount {
   340  	var mounts = []*harness.Mount{
   341  		{
   342  			Name: "dockersock",
   343  			Path: "/var/run/docker.sock",
   344  		},
   345  	}
   346  	for _, vol := range srcstep.Volumes {
   347  		mounts = append(mounts, &harness.Mount{
   348  			Name: vol.Name,
   349  			Path: vol.Path,
   350  		})
   351  	}
   352  	if src.Options != nil {
   353  		for _, vol := range src.Options.Volumes {
   354  			mounts = append(mounts, &harness.Mount{
   355  				Name: vol.Name,
   356  				Path: vol.Path,
   357  			})
   358  		}
   359  	}
   360  	return mounts
   361  }
   362  
   363  // helper function returns a timeout string. If there
   364  // is no timeout, a zero value is returned.
   365  func convertTimeout(src time.Duration) string {
   366  	if dst := src.String(); dst == "0s" {
   367  		return ""
   368  	} else {
   369  		return dst
   370  	}
   371  }
   372  
   373  // helper function returns a machine size that corresponds
   374  // to the google cloud machine type.
   375  func convertMachine(src string) string {
   376  	switch src {
   377  	case "N1_HIGHCPU_8", "E2_HIGHCPU_8":
   378  		return "standard"
   379  	case "N1_HIGHCPU_32", "E2_HIGHCPU_32":
   380  		return "" // TODO convert 32 core machines
   381  	default:
   382  		return ""
   383  	}
   384  }
   385  
   386  // helper function that converts a string slice of
   387  // environment variables in key=value format to a map.
   388  func convertEnv(src []string) map[string]string {
   389  	dst := map[string]string{}
   390  	for _, env := range src {
   391  		parts := strings.SplitN(env, "=", 2)
   392  		if len(parts) != 2 {
   393  			continue
   394  		}
   395  		k := parts[0]
   396  		v := parts[1]
   397  		dst[k] = v
   398  	}
   399  	if len(dst) == 0 {
   400  		return nil
   401  	} else {
   402  		return dst
   403  	}
   404  }
   405  
   406  // helper function combines one or more maps of environment
   407  // variables into a single map.
   408  func combineEnv(env ...map[string]string) map[string]string {
   409  	c := map[string]string{}
   410  	for _, e := range env {
   411  		for k, v := range e {
   412  			c[k] = v
   413  		}
   414  	}
   415  	return c
   416  }
   417  
   418  // helper function maps input variables to expressions.
   419  func mapInputsToExpr(envs map[string]string) map[string]string {
   420  	out := map[string]string{}
   421  	for k := range envs {
   422  		out[k] = "<+inputs." + k + ">"
   423  	}
   424  	return out
   425  }
   426  
   427  func replaceAll(in *harness.Config, envs map[string]string) (*harness.Config, error) {
   428  	// marshal the harness yaml
   429  	b, err := yaml.Marshal(in)
   430  	if err != nil {
   431  		return in, err
   432  	}
   433  
   434  	// find and replace google cloudbuild variables with
   435  	// the harness equivalents.
   436  	for before, after := range envs {
   437  		b = bytes.ReplaceAll(b, []byte("${"+before+"}"), []byte(after))
   438  	}
   439  
   440  	// unarmarshal the yaml
   441  	out, err := harness.ParseBytes(b)
   442  	if err != nil {
   443  		return in, err
   444  	}
   445  	return out, nil
   446  }