diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/client/model/v3/Config.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/client/model/v3/Config.java index 0e8b8aaa8..a730c355f 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/client/model/v3/Config.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/client/model/v3/Config.java @@ -17,23 +17,30 @@ package com.netflix.spinnaker.igor.travis.client.model.v3; +import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; import com.netflix.spinnaker.igor.build.model.GenericParameterDefinition; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.simpleframework.xml.Default; @Default @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) +@Slf4j public class Config { - @JsonProperty("global_env") private List globalEnv; @JsonProperty("merge_mode") @@ -75,12 +82,34 @@ public List getParameterDefinitionList() { .collect(Collectors.toList()); } + @JsonGetter("global_env") public List getGlobalEnv() { return globalEnv; } - public void setGlobalEnv(List globalEnv) { - this.globalEnv = globalEnv; + @JsonSetter("global_env") + @SuppressWarnings("unchecked") + public void setGlobalEnv(Object globalEnv) { + if (globalEnv instanceof String) { + // Matches space separated KEY=VALUE pairs. See ConfigSpec for matching examples + Pattern pattern = Pattern.compile("(\\S*?)=(?>(?>[\"'])(.*?)(?>[\"'])|(\\S*))"); + Matcher matcher = pattern.matcher((String) globalEnv); + List env = new ArrayList<>(); + while (matcher.find()) { + String key = matcher.group(1); + String value = matcher.group(2); + if (StringUtils.isBlank(value)) { + value = matcher.group(3); + } + value = StringUtils.trimToEmpty(value); + env.add(key + "=" + value); + } + this.globalEnv = new ArrayList<>(env); + } else if (globalEnv instanceof List) { + this.globalEnv = (List) globalEnv; + } else { + log.warn("Unknown type for 'global_env', ignoring: {}", globalEnv); + } } public String getMergeMode() { diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/TravisClientSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/TravisClientSpec.groovy index 5db106088..d4516fec2 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/TravisClientSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/TravisClientSpec.groovy @@ -694,6 +694,130 @@ class TravisClientSpec extends Specification { } + def "should parse v3 config section correctly"() { + given: + setResponse '''{ + "@type": "builds", + "@href": "/api/repo/my-org%2Fmy-project/builds?limit=1&include=job.config", + "@representation": "standard", + "@pagination": { + "limit": 1, + "offset": 0, + "count": 1213, + "is_first": true, + "is_last": false, + "next": { + "@href": "/api/repo/my-org%2Fmy-project/builds?include=job.config&limit=1&offset=1", + "offset": 1, + "limit": 1 + }, + "prev": null, + "first": { + "@href": "/api/repo/my-org%2Fmy-project/builds?limit=1&include=job.config", + "offset": 0, + "limit": 1 + }, + "last": { + "@href": "/api/repo/my-org%2Fmy-project/builds?include=job.config&limit=1&offset=1212", + "offset": 1212, + "limit": 1 + } + }, + "builds": [ + { + "@type": "build", + "@href": "/api/build/8078881", + "@representation": "standard", + "@permissions": { + "read": true, + "cancel": true, + "restart": true + }, + "id": 8078881, + "number": "1213", + "state": "errored", + "duration": 19, + "event_type": "cron", + "previous_state": "failed", + "pull_request_title": null, + "pull_request_number": null, + "started_at": "2019-09-04T10:47:53Z", + "finished_at": "2019-09-04T10:48:12Z", + "repository": { + "@type": "repository", + "@href": "/api/repo/10094", + "@representation": "minimal", + "id": 10094, + "name": "my-project", + "slug": "my-org/my-project" + }, + "branch": { + "@type": "branch", + "@href": "/api/repo/10094/branch/master", + "@representation": "minimal", + "name": "master" + }, + "tag": null, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 4571457, + "sha": "8af6ccfec7ea3cac6c3e01751186bc9ca5b6062e", + "ref": null, + "message": "Release 0.1.28", + "compare_url": "https://github.acme.com/my-org/my-project/compare/7a7214f554e2b8e313354af08c1010ad90f1e612...8af6ccfec7ea3cac6c3e01751186bc9ca5b6062e", + "committed_at": "2019-04-24T21:54:13Z" + }, + "jobs": [ + { + "@type": "job", + "@href": "/api/job/8078882", + "@representation": "minimal", + "id": 8078882, + "config": { + "language": "python", + "cache": "pip", + "install": "script/bootstrap", + "script": "script/cibuild", + "after_script": "script/cleanup", + ".result": "configured", + "global_env": "REGION=eu-west-1 STACK_NAME=testing A_USER=user@schibsted.com A_PWD=[secure] KEY_ID=\\"MY_KEY\\" A_SECRET=[secure] ROLE='arn:aws:iam::0123456789:role/myRole'", + "os": "linux", + "group": "stable", + "dist": "trusty", + "addons": {} + } + } + ], + "stages": [], + "created_by": { + "@type": "user", + "@href": "/api/user/574", + "@representation": "minimal", + "id": 574, + "login": "spinnaker" + }, + "updated_at": "2019-09-04T10:48:12.782Z" + } + ] + }''' + + when: + V3Builds builds = client.v3builds("someToken", "org/repo", 1, "job.config") + + then: + builds.builds.size() == 1 + builds.builds[0].jobs[0].config.globalEnv == [ + "REGION=eu-west-1", + "STACK_NAME=testing", + "A_USER=user@schibsted.com", + "A_PWD=[secure]", + "KEY_ID=MY_KEY", + "A_SECRET=[secure]", + "ROLE=arn:aws:iam::0123456789:role/myRole" + ] + } + private void setResponse(String body) { server.enqueue( new MockResponse() diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/model/v3/ConfigSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/model/v3/ConfigSpec.groovy index 52b806b49..baa903afd 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/model/v3/ConfigSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/travis/client/model/v3/ConfigSpec.groovy @@ -78,4 +78,44 @@ class ConfigSpec extends Specification { null || null } + @Unroll + def "should handle different env_var types"() { + given: + Config config = new Config() + + when: + config.setGlobalEnv(globalEnv) + + then: + config.globalEnv == expectedGlobalEnv + + where: + globalEnv || expectedGlobalEnv + ["KEY_1=value 1", "KEY_2=value 2"] || ["KEY_1=value 1", "KEY_2=value 2"] + "TF_INPUT=false " + + "SOME_KEY=\"with spaces\" " + + "ANOTHER_KEY=withoutspaces " + + "lowercase-key=\"string with=equals sign\" " + + "REGION=eu-west-1 " + + "STACK_NAME=testing " + + "A_USER=user@schibsted.com " + + "A_PWD=[secure] " + + "KEY_ID=\"SOMEKEYID\" " + + "SOME_SECRET=[secure] " + + "ROLE='arn:aws:iam::0123456789:role/MyRole' " + + "KEY=whatabout=this\\\\ " + + "KEY2=\"and=this\\\\\"" || ["TF_INPUT=false", + "SOME_KEY=with spaces", + "ANOTHER_KEY=withoutspaces", + "lowercase-key=string with=equals sign", + "REGION=eu-west-1", + "STACK_NAME=testing", + "A_USER=user@schibsted.com", + "A_PWD=[secure]", + "KEY_ID=SOMEKEYID", + "SOME_SECRET=[secure]", + "ROLE=arn:aws:iam::0123456789:role/MyRole", + "KEY=whatabout=this\\\\", + "KEY2=and=this\\\\"] + } }