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 }