diff --git a/README.md b/README.md index 68e6d9c..f2e52f6 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,14 @@ All sinks must have the following fields in addition to any sink-specific fields [`TRAVIS_API_AUTH_TOKEN`](https://github.com/shuheiktgw/go-travis#authentication-with-travis-api-token) should be set. +### Circle CI (`CircleCI`) +| Name | Description | Required | +|------|-------------|:-----:| +| account | The CircleCI account to write this env var to. | yes | +| repo | The CircleCI repo to write this env var to. | yes | + +[`CIRCLECI_AUTH_TOKEN`](https://circleci.com/docs/2.0/managing-api-tokens/) must be set. + ### AWS Systems Manager Parameter Store (`AWSParameterStore`) | Name | Description | Required | |------|-------------|:-----:| diff --git a/go.mod b/go.mod index cdec885..f7de17e 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/chanzuckerberg/rotator go 1.13 require ( - github.com/aws/aws-sdk-go v1.28.9 + github.com/aws/aws-sdk-go v1.29.5 github.com/chanzuckerberg/go-misc v0.0.0-20191125152854-38391fa92cd1 github.com/fatih/color v1.9.0 - github.com/getsentry/sentry-go v0.5.0 + github.com/getsentry/sentry-go v0.5.1 github.com/google/uuid v1.1.1 github.com/hashicorp/go-multierror v1.0.0 github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect + github.com/jszwedko/go-circleci v0.3.0 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/pkg/errors v0.9.1 @@ -19,9 +20,9 @@ require ( github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.2.0 // indirect - github.com/stretchr/testify v1.4.0 - golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d // indirect - golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect + github.com/stretchr/testify v1.5.0 + golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 // indirect + golang.org/x/sys v0.0.0-20200217220822-9197077df867 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 986959c..cfb53bd 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-lambda-go v1.12.1/go.mod h1:z4ywteZ5WwbIEzG0tXizIAUlUwkTNNknX4upd5Z5XJM= github.com/aws/aws-sdk-go v1.23.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0= -github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.29.5 h1:PddgnlgWgNI6x/weTnfk1fGYkhcs363gieDzK+Cf91Q= +github.com/aws/aws-sdk-go v1.29.5/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/chanzuckerberg/go-misc v0.0.0-20191125152854-38391fa92cd1 h1:op+eO8J7JrwaAcJBNW0OKk7yLxo680MrPuVXaOerq08= @@ -43,8 +43,8 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= -github.com/getsentry/sentry-go v0.5.0 h1:TvIiQJIm9lQj8v4CGPL3oSfN+hqc6PTfEBBbt3rrE3c= -github.com/getsentry/sentry-go v0.5.0/go.mod h1:B8H7x8TYDPkeWPRzGpIiFO97LZP6rL8A3hEt8lUItMw= +github.com/getsentry/sentry-go v0.5.1 h1:MIPe7ScHADsrK2vznqmhksIUFxq7m0JfTh+ZIMkI+VQ= +github.com/getsentry/sentry-go v0.5.1/go.mod h1:B8H7x8TYDPkeWPRzGpIiFO97LZP6rL8A3hEt8lUItMw= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= @@ -53,6 +53,7 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= @@ -90,6 +91,8 @@ github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrO github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jszwedko/go-circleci v0.3.0 h1:zmYFSb2NlSvUvXydYcJY2AF6n88LGa+5teZtCm2pmmk= +github.com/jszwedko/go-circleci v0.3.0/go.mod h1:z1630OiB7oGxZwE90het04Ld7rIu0AKvY9JCRnaBdoE= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= @@ -180,6 +183,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw= +github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -203,8 +208,8 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -217,6 +222,8 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -232,8 +239,8 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/pkg/config/config.go b/pkg/config/config.go index 47afdf9..366a684 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -13,12 +13,18 @@ import ( "github.com/chanzuckerberg/rotator/pkg/sink" "github.com/chanzuckerberg/rotator/pkg/source" "github.com/hashicorp/go-multierror" + "github.com/jszwedko/go-circleci" "github.com/pkg/errors" "github.com/shuheiktgw/go-travis" "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" ) +const ( + envCircleCIAuthToken = "CIRCLECI_AUTH_TOKEN" + envTravisCIAuthToken = "TRAVIS_API_AUTH_TOKEN" +) + type Config struct { Version int `yaml:"version"` Secrets []Secret `yaml:"secrets"` @@ -156,13 +162,29 @@ func unmarshalSinks(sinksIface interface{}) (sink.Sinks, error) { // set up Travis CI API client client := travis.NewClient(sink.TravisBaseURL, "") - travisToken := os.Getenv("TRAVIS_API_AUTH_TOKEN") + travisToken, present := os.LookupEnv(envTravisCIAuthToken) + if !present { + return nil, errors.Errorf("missing env var: %s", envTravisCIAuthToken) + } err := client.Authentication.UsingTravisToken(travisToken) if err != nil { return nil, errors.Wrap(err, "unable to authenticate travis API") } - sinks = append(sinks, &sink.TravisCiSink{BaseSink: sink.BaseSink{KeyToName: keyToName}, RepoSlug: sinkMapStr["repo_slug"], Client: client}) + + case sink.KindCircleCi: + if err = validate(sinkMapStr, "account", "repo"); err != nil { + return nil, errors.Wrap(err, "missing keys in circle CI sink config") + } + circleToken, present := os.LookupEnv(envCircleCIAuthToken) + if !present { + return nil, errors.Errorf("missing env var: %s", envCircleCIAuthToken) + } + client := &circleci.Client{Token: circleToken} + sink := &sink.CircleCiSink{BaseSink: sink.BaseSink{KeyToName: keyToName}} + sink.WithCircleClient(client, sinkMapStr["account"], sinkMapStr["repo"]) + sinks = append(sinks, sink) + case sink.KindAwsParamStore: if err = validate(sinkMapStr, "role_arn", "region"); err != nil { return nil, errors.Wrap(err, "missing keys in aws parameter store sink config") diff --git a/pkg/sink/circleCi.go b/pkg/sink/circleCi.go new file mode 100644 index 0000000..bcd594e --- /dev/null +++ b/pkg/sink/circleCi.go @@ -0,0 +1,46 @@ +package sink + +import ( + "context" + "time" + + "github.com/jszwedko/go-circleci" + "github.com/pkg/errors" +) + +const ( + circleRetryAttempts = 5 + circleRetrySleep = time.Second +) + +// CircleCiSink is a circleci sink +type CircleCiSink struct { + BaseSink `yaml:",inline"` + + Client *circleci.Client + Account string + Repo string +} + +// WithCircleClient configures a circleci client for this sink +func (sink *CircleCiSink) WithCircleClient(client *circleci.Client, account string, repo string) *CircleCiSink { + sink.Client = client + sink.Account = account + sink.Repo = repo + + return sink +} + +// Write writes the value of the env var with the specified name for the given repo +func (sink *CircleCiSink) Write(ctx context.Context, name string, val string) error { + f := func(ctx context.Context) error { + _, err := sink.Client.AddEnvVar(sink.Account, sink.Repo, name, val) + return errors.Wrapf(err, "could not write %s to %s/%s", name, sink.Account, sink.Repo) + } + return retry(ctx, circleRetryAttempts, circleRetrySleep, f) +} + +// Kind returns the kind of this sink +func (sink *CircleCiSink) Kind() Kind { + return KindCircleCi +} diff --git a/pkg/sink/circleCi_test.go b/pkg/sink/circleCi_test.go new file mode 100644 index 0000000..3260c23 --- /dev/null +++ b/pkg/sink/circleCi_test.go @@ -0,0 +1,89 @@ +package sink_test + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/chanzuckerberg/rotator/pkg/sink" + "github.com/jszwedko/go-circleci" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +const ( + circleAccount = "testo_account" + circleRepo = "testo_repo" + circleBadRepo = "bad_testo_repo" + circleEnvVar = "foo" + circleEnvVarVal = "bar" +) + +type CircleTestSuite struct { + suite.Suite + + ctx context.Context + client *circleci.Client + mux *http.ServeMux + teardown func() +} + +func (ts *CircleTestSuite) TearDownTest() { + ts.teardown() +} + +func (ts *CircleTestSuite) SetupTest() { + mux := http.NewServeMux() + apiHandler := http.NewServeMux() + apiHandler.Handle("/", mux) + + server := httptest.NewServer(apiHandler) + u, _ := url.Parse(server.URL + "/") + client := &circleci.Client{BaseURL: u} + + ts.ctx = context.Background() + ts.client = client + ts.mux = mux + ts.teardown = server.Close + + t := ts.T() + a := assert.New(t) + + mux.HandleFunc( + fmt.Sprintf("/project/%s/%s/envvar", circleAccount, circleRepo), + func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + a.Fail("invalid http method %s", r.Method) + return + } + + b, err := ioutil.ReadAll(r.Body) + a.NoError(err) + + want := fmt.Sprintf(`{"name":"%s","value":"%s"}`, circleEnvVar, circleEnvVarVal) + if want != string(b) { + http.Error(w, "body doesn't match", http.StatusBadRequest) + } + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, want) + return + }, + ) +} + +func (ts *CircleTestSuite) TestWriteToCircleCiSink() { + t := ts.T() + a := assert.New(t) + sink := &sink.CircleCiSink{} + sink.WithCircleClient(ts.client, circleAccount, circleRepo) + err := sink.Write(ts.ctx, circleEnvVar, circleEnvVarVal) + a.NoError(err) +} + +func TestCircleCISuite(t *testing.T) { + suite.Run(t, new(CircleTestSuite)) +} diff --git a/pkg/sink/sink.go b/pkg/sink/sink.go index 49b66b5..fd52f9d 100644 --- a/pkg/sink/sink.go +++ b/pkg/sink/sink.go @@ -29,6 +29,7 @@ func (e Error) Error() string { return string(e) } const ( KindBuf Kind = "Buffer" KindTravisCi Kind = "TravisCI" + KindCircleCi = "CircleCI" KindAwsParamStore Kind = "AWSParameterStore" KindAwsSecretsManager Kind = "AWSSecretsManager" KindStdout Kind = "Stdout" @@ -74,6 +75,15 @@ func (sinks Sinks) MarshalYAML() (interface{}, error) { "external_id": sink.ExternalID, "region": sink.Region, }) + case KindCircleCi: + sink := s.(*CircleCiSink) + yamlSinks = append(yamlSinks, + map[string]interface{}{ + "kind": string(KindCircleCi), + "key_to_name": sink.KeyToName, + "account": sink.Account, + "repo": sink.Repo, + }) default: return nil, ErrUnknownKind } diff --git a/pkg/sink/travisCi.go b/pkg/sink/travisCi.go index 330e1be..f6a17e2 100644 --- a/pkg/sink/travisCi.go +++ b/pkg/sink/travisCi.go @@ -18,7 +18,7 @@ const ( travisRetrySleep = time.Second ) -// TravisCiSink returns the +// TravisCiSink is a travisCi sink type TravisCiSink struct { BaseSink `yaml:",inline"` @@ -70,7 +70,6 @@ func (sink *TravisCiSink) create(ctx context.Context, body *travis.EnvVarBody) e } return nil } - return retry(ctx, travisRetryAttempts, travisRetrySleep, f) } diff --git a/pkg/sink/travisCi_test.go b/pkg/sink/travisCi_test.go index 1251ea4..9b66070 100644 --- a/pkg/sink/travisCi_test.go +++ b/pkg/sink/travisCi_test.go @@ -45,7 +45,7 @@ func (ts *TravisTestSuite) TearDownTest() { func (ts *TravisTestSuite) SetupTest() { ts.ctx = context.Background() ts.client, ts.mux, _, ts.teardown = setup() - t := ts.T() + t := ts.T().(*testing.T) ts.mux.HandleFunc(fmt.Sprintf("/repo/%s/env_vars", testRepoSlug), func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { diff --git a/vendor/github.com/jszwedko/go-circleci/CHANGELOG.md b/vendor/github.com/jszwedko/go-circleci/CHANGELOG.md new file mode 100644 index 0000000..38b267c --- /dev/null +++ b/vendor/github.com/jszwedko/go-circleci/CHANGELOG.md @@ -0,0 +1,36 @@ +# Change Log + +**ATTN**: This project uses [semantic versioning](http://semver.org/). + +## [Unreleased] + +## 0.3.0 - 2019-06-29 + +Added: + +* `Build`s now return `Workflow` information +* `Build`s now return `Picard` information which describes the execution environment +* `Build`s now return `Platform` information +* `Action`s now return `Background` information +* `CommitDetails` now return `Branch` and `PullRequest` +* `BuildOpts` method for building a project with arbitrary parameters + +Bug fixes: + +* Fix issue with paginating queries returning a 401 +* Actually send build parameters for `ParameterizedBuild` +* Fix feature flag parsing to not cause a null pointer panic + +## 0.2.0 - 2018-03-10 + +Backwards incomptabile changes: + +* Switched `FeatureFlags` to a `struct` from `map[string]bool` as it was found that not all feature flags are `bool`s + (https://github.com/jszwedko/go-circleci/issues/8) which resulted in non-bool values being inaccessible. Known feature + flags are encoded as struct fields with a `.Raw()` method to access the underlying `map[string]interface{}` to access + unknown feature flags. + +## 0.1.0 - 2018-03-10 + +### Added +- Initial implementation. diff --git a/vendor/github.com/jszwedko/go-circleci/LICENSE b/vendor/github.com/jszwedko/go-circleci/LICENSE new file mode 100644 index 0000000..6282fac --- /dev/null +++ b/vendor/github.com/jszwedko/go-circleci/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jesse Szwedko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/jszwedko/go-circleci/README.md b/vendor/github.com/jszwedko/go-circleci/README.md new file mode 100644 index 0000000..2c7dddb --- /dev/null +++ b/vendor/github.com/jszwedko/go-circleci/README.md @@ -0,0 +1,43 @@ +## go-circleci +[![GoDoc](https://godoc.org/github.com/jszwedko/go-circleci?status.svg)](http://godoc.org/github.com/jszwedko/go-circleci) +[![Circle CI](https://circleci.com/gh/jszwedko/go-circleci.svg?style=svg)](https://circleci.com/gh/jszwedko/go-circleci) +[![Go Report Card](https://goreportcard.com/badge/github.com/jszwedko/go-circleci)](https://goreportcard.com/report/github.com/jszwedko/go-circleci) +[![coverage](https://gocover.io/_badge/github.com/jszwedko/go-circleci?0 "coverage")](http://gocover.io/github.com/jszwedko/go-circleci) + +Go library for interacting with [CircleCI's API](https://circleci.com/docs/api). Supports all current API endpoints allowing you do do things like: + +* Query for recent builds +* Get build details +* Retry builds +* Manipulate checkout keys, environment variables, and other settings for a project + +**The CircleCI HTTP API response schemas are not well documented so please file an issue if you run into something that doesn't match up.** + +Example usage: + +```golang +package main + +import ( + "fmt" + + "github.com/jszwedko/go-circleci" +) + +func main() { + client := &circleci.Client{Token: "YOUR TOKEN"} // Token not required to query info for public projects + + builds, _ := client.ListRecentBuildsForProject("jszwedko", "circleci-cli", "master", "", -1, 0) + + for _, build := range builds { + fmt.Printf("%d: %s\n", build.BuildNum, build.Status) + } +} +``` + +For the CLI that uses this library (or to see more example usages), please see +[circleci-cli](https://github.com/jszwedko/circleci-cli). + +See [GoDoc](http://godoc.org/github.com/jszwedko/go-circleci) for API usage. + +Feature requests and issues welcome! diff --git a/vendor/github.com/jszwedko/go-circleci/circleci.go b/vendor/github.com/jszwedko/go-circleci/circleci.go new file mode 100644 index 0000000..b478958 --- /dev/null +++ b/vendor/github.com/jszwedko/go-circleci/circleci.go @@ -0,0 +1,1013 @@ +package circleci + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strconv" + "time" +) + +const ( + queryLimit = 100 // maximum that CircleCI allows +) + +var ( + defaultBaseURL = &url.URL{Host: "circleci.com", Scheme: "https", Path: "/api/v1/"} + defaultLogger = log.New(os.Stderr, "", log.LstdFlags) +) + +// Logger is a minimal interface for injecting custom logging logic for debug logs +type Logger interface { + Printf(fmt string, args ...interface{}) +} + +// APIError represents an error from CircleCI +type APIError struct { + HTTPStatusCode int + Message string +} + +func (e *APIError) Error() string { + return fmt.Sprintf("%d: %s", e.HTTPStatusCode, e.Message) +} + +// Client is a CircleCI client +// Its zero value is a usable client for examining public CircleCI repositories +type Client struct { + BaseURL *url.URL // CircleCI API endpoint (defaults to DefaultEndpoint) + Token string // CircleCI API token (needed for private repositories and mutative actions) + HTTPClient *http.Client // HTTPClient to use for connecting to CircleCI (defaults to http.DefaultClient) + + Debug bool // debug logging enabled + Logger Logger // logger to send debug messages on (if enabled), defaults to logging to stderr with the standard flags +} + +func (c *Client) baseURL() *url.URL { + if c.BaseURL == nil { + return defaultBaseURL + } + + return c.BaseURL +} + +func (c *Client) client() *http.Client { + if c.HTTPClient == nil { + return http.DefaultClient + } + + return c.HTTPClient +} + +func (c *Client) logger() Logger { + if c.Logger == nil { + return defaultLogger + } + + return c.Logger +} + +func (c *Client) debug(format string, args ...interface{}) { + if c.Debug { + c.logger().Printf(format, args...) + } +} + +func (c *Client) debugRequest(req *http.Request) { + if c.Debug { + out, err := httputil.DumpRequestOut(req, true) + if err != nil { + c.debug("error debugging request %+v: %s", req, err) + } + c.debug("request:\n%+v", string(out)) + } +} + +func (c *Client) debugResponse(resp *http.Response) { + if c.Debug { + out, err := httputil.DumpResponse(resp, true) + if err != nil { + c.debug("error debugging response %+v: %s", resp, err) + } + c.debug("response:\n%+v", string(out)) + } +} + +type nopCloser struct { + io.Reader +} + +func (n nopCloser) Close() error { return nil } + +func (c *Client) request(method, path string, responseStruct interface{}, params url.Values, bodyStruct interface{}) error { + if params == nil { + params = url.Values{} + } + params.Set("circle-token", c.Token) + + u := c.baseURL().ResolveReference(&url.URL{Path: path, RawQuery: params.Encode()}) + + c.debug("building request for %s", u) + + req, err := http.NewRequest(method, u.String(), nil) + if err != nil { + return err + } + + if bodyStruct != nil { + b, err := json.Marshal(bodyStruct) + if err != nil { + return err + } + + req.Body = nopCloser{bytes.NewBuffer(b)} + } + + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + + c.debugRequest(req) + + resp, err := c.client().Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + c.debugResponse(resp) + + if resp.StatusCode >= 300 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return &APIError{HTTPStatusCode: resp.StatusCode, Message: "unable to parse response: %s"} + } + + if len(body) > 0 { + message := struct { + Message string `json:"message"` + }{} + err = json.Unmarshal(body, &message) + if err != nil { + return &APIError{ + HTTPStatusCode: resp.StatusCode, + Message: fmt.Sprintf("unable to parse API response: %s", err), + } + } + return &APIError{HTTPStatusCode: resp.StatusCode, Message: message.Message} + } + + return &APIError{HTTPStatusCode: resp.StatusCode} + } + + if responseStruct != nil { + err = json.NewDecoder(resp.Body).Decode(responseStruct) + if err != nil { + return err + } + } + + return nil +} + +// Me returns information about the current user +func (c *Client) Me() (*User, error) { + user := &User{} + + err := c.request("GET", "me", user, nil, nil) + if err != nil { + return nil, err + } + + return user, nil +} + +// ListProjects returns the list of projects the user is watching +func (c *Client) ListProjects() ([]*Project, error) { + projects := []*Project{} + + err := c.request("GET", "projects", &projects, nil, nil) + if err != nil { + return nil, err + } + + for _, project := range projects { + if err := cleanupProject(project); err != nil { + return nil, err + } + } + + return projects, nil +} + +// EnableProject enables a project - generates a deploy SSH key used to checkout the Github repo. +// The Github user tied to the Circle API Token must have "admin" access to the repo. +func (c *Client) EnableProject(account, repo string) error { + return c.request("POST", fmt.Sprintf("project/%s/%s/enable", account, repo), nil, nil, nil) +} + +// DisableProject disables a project +func (c *Client) DisableProject(account, repo string) error { + return c.request("DELETE", fmt.Sprintf("project/%s/%s/enable", account, repo), nil, nil, nil) +} + +// FollowProject follows a project +func (c *Client) FollowProject(account, repo string) (*Project, error) { + project := &Project{} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/follow", account, repo), project, nil, nil) + if err != nil { + return nil, err + } + + if err := cleanupProject(project); err != nil { + return nil, err + } + + return project, nil +} + +// GetProject retrieves a specific project +// Returns nil of the project is not in the list of watched projects +func (c *Client) GetProject(account, repo string) (*Project, error) { + projects, err := c.ListProjects() + if err != nil { + return nil, err + } + + for _, project := range projects { + if account == project.Username && repo == project.Reponame { + return project, nil + } + } + + return nil, nil +} + +func (c *Client) recentBuilds(path string, params url.Values, limit, offset int) ([]*Build, error) { + allBuilds := []*Build{} + + if params == nil { + params = url.Values{} + } + + fetchAll := limit == -1 + for { + builds := []*Build{} + + if fetchAll { + limit = queryLimit + 1 + } + + l := limit + if l > queryLimit { + l = queryLimit + } + + params.Set("limit", strconv.Itoa(l)) + params.Set("offset", strconv.Itoa(offset)) + + err := c.request("GET", path, &builds, params, nil) + if err != nil { + return nil, err + } + allBuilds = append(allBuilds, builds...) + + offset += len(builds) + limit -= len(builds) + if len(builds) < queryLimit || limit == 0 { + break + } + } + + return allBuilds, nil +} + +// ListRecentBuilds fetches the list of recent builds for all repositories the user is watching +// If limit is -1, fetches all builds +func (c *Client) ListRecentBuilds(limit, offset int) ([]*Build, error) { + return c.recentBuilds("recent-builds", nil, limit, offset) +} + +// ListRecentBuildsForProject fetches the list of recent builds for the given repository +// The status and branch parameters are used to further filter results if non-empty +// If limit is -1, fetches all builds +func (c *Client) ListRecentBuildsForProject(account, repo, branch, status string, limit, offset int) ([]*Build, error) { + path := fmt.Sprintf("project/%s/%s", account, repo) + if branch != "" { + path = fmt.Sprintf("%s/tree/%s", path, branch) + } + + params := url.Values{} + if status != "" { + params.Set("filter", status) + } + + return c.recentBuilds(path, params, limit, offset) +} + +// GetBuild fetches a given build by number +func (c *Client) GetBuild(account, repo string, buildNum int) (*Build, error) { + build := &Build{} + + err := c.request("GET", fmt.Sprintf("project/%s/%s/%d", account, repo, buildNum), build, nil, nil) + if err != nil { + return nil, err + } + + return build, nil +} + +// ListBuildArtifacts fetches the build artifacts for the given build +func (c *Client) ListBuildArtifacts(account, repo string, buildNum int) ([]*Artifact, error) { + artifacts := []*Artifact{} + + err := c.request("GET", fmt.Sprintf("project/%s/%s/%d/artifacts", account, repo, buildNum), &artifacts, nil, nil) + if err != nil { + return nil, err + } + + return artifacts, nil +} + +// ListTestMetadata fetches the build metadata for the given build +func (c *Client) ListTestMetadata(account, repo string, buildNum int) ([]*TestMetadata, error) { + metadata := struct { + Tests []*TestMetadata `json:"tests"` + }{} + + err := c.request("GET", fmt.Sprintf("project/%s/%s/%d/tests", account, repo, buildNum), &metadata, nil, nil) + if err != nil { + return nil, err + } + + return metadata.Tests, nil +} + +// AddSSHUser adds the user associated with the API token to the list of valid +// SSH users for a build. +// +// The API token being used must be a user API token +func (c *Client) AddSSHUser(account, repo string, buildNum int) (*Build, error) { + build := &Build{} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/%d/ssh-users", account, repo, buildNum), build, nil, nil) + if err != nil { + return nil, err + } + + return build, nil +} + +// Build triggers a new build for the given project for the given +// project on the given branch. +// Returns the new build information +func (c *Client) Build(account, repo, branch string) (*Build, error) { + return c.BuildOpts(account, repo, branch, nil) +} + +// ParameterizedBuild triggers a new parameterized build for the given +// project on the given branch, Marshaling the struct into json and passing +// in the post body. +// Returns the new build information +func (c *Client) ParameterizedBuild(account, repo, branch string, buildParameters map[string]string) (*Build, error) { + opts := map[string]interface{}{"build_parameters": buildParameters} + return c.BuildOpts(account, repo, branch, opts) +} + +// BuildOpts triggeres a new build for the givent project on the given +// branch, Marshaling the struct into json and passing +// in the post body. +// Returns the new build information +func (c *Client) BuildOpts(account, repo, branch string, opts map[string]interface{}) (*Build, error) { + build := &Build{} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/tree/%s", account, repo, branch), build, nil, opts) + if err != nil { + return nil, err + } + + return build, nil +} + +// RetryBuild triggers a retry of the specified build +// Returns the new build information +func (c *Client) RetryBuild(account, repo string, buildNum int) (*Build, error) { + build := &Build{} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/%d/retry", account, repo, buildNum), build, nil, nil) + if err != nil { + return nil, err + } + + return build, nil +} + +// CancelBuild triggers a cancel of the specified build +// Returns the new build information +func (c *Client) CancelBuild(account, repo string, buildNum int) (*Build, error) { + build := &Build{} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/%d/cancel", account, repo, buildNum), build, nil, nil) + if err != nil { + return nil, err + } + + return build, nil +} + +// ClearCache clears the cache of the specified project +// Returns the status returned by CircleCI +func (c *Client) ClearCache(account, repo string) (string, error) { + status := &struct { + Status string `json:"status"` + }{} + + err := c.request("DELETE", fmt.Sprintf("project/%s/%s/build-cache", account, repo), status, nil, nil) + if err != nil { + return "", err + } + + return status.Status, nil +} + +// AddEnvVar adds a new environment variable to the specified project +// Returns the added env var (the value will be masked) +func (c *Client) AddEnvVar(account, repo, name, value string) (*EnvVar, error) { + envVar := &EnvVar{} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/envvar", account, repo), envVar, nil, &EnvVar{Name: name, Value: value}) + if err != nil { + return nil, err + } + + return envVar, nil +} + +// ListEnvVars list environment variable to the specified project +// Returns the env vars (the value will be masked) +func (c *Client) ListEnvVars(account, repo string) ([]EnvVar, error) { + envVar := []EnvVar{} + + err := c.request("GET", fmt.Sprintf("project/%s/%s/envvar", account, repo), &envVar, nil, nil) + if err != nil { + return nil, err + } + + return envVar, nil +} + +// DeleteEnvVar deletes the specified environment variable from the project +func (c *Client) DeleteEnvVar(account, repo, name string) error { + return c.request("DELETE", fmt.Sprintf("project/%s/%s/envvar/%s", account, repo, name), nil, nil, nil) +} + +// AddSSHKey adds a new SSH key to the project +func (c *Client) AddSSHKey(account, repo, hostname, privateKey string) error { + key := &struct { + Hostname string `json:"hostname"` + PrivateKey string `json:"private_key"` + }{hostname, privateKey} + return c.request("POST", fmt.Sprintf("project/%s/%s/ssh-key", account, repo), nil, nil, key) +} + +// GetActionOutputs fetches the output for the given action +// If the action has no output, returns nil +func (c *Client) GetActionOutputs(a *Action) ([]*Output, error) { + if !a.HasOutput || a.OutputURL == "" { + return nil, nil + } + + req, err := http.NewRequest("GET", a.OutputURL, nil) + if err != nil { + return nil, err + } + + c.debugRequest(req) + + resp, err := c.client().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + c.debugResponse(resp) + + output := []*Output{} + if err = json.NewDecoder(resp.Body).Decode(&output); err != nil { + return nil, err + } + + return output, nil +} + +// ListCheckoutKeys fetches the checkout keys associated with the given project +func (c *Client) ListCheckoutKeys(account, repo string) ([]*CheckoutKey, error) { + checkoutKeys := []*CheckoutKey{} + + err := c.request("GET", fmt.Sprintf("project/%s/%s/checkout-key", account, repo), &checkoutKeys, nil, nil) + if err != nil { + return nil, err + } + + return checkoutKeys, nil +} + +// CreateCheckoutKey creates a new checkout key for a project +// Valid key types are currently deploy-key and github-user-key +// +// The github-user-key type requires that the API token being used be a user API token +func (c *Client) CreateCheckoutKey(account, repo, keyType string) (*CheckoutKey, error) { + checkoutKey := &CheckoutKey{} + + body := struct { + KeyType string `json:"type"` + }{KeyType: keyType} + + err := c.request("POST", fmt.Sprintf("project/%s/%s/checkout-key", account, repo), checkoutKey, nil, body) + if err != nil { + return nil, err + } + + return checkoutKey, nil +} + +// GetCheckoutKey fetches the checkout key for the given project by fingerprint +func (c *Client) GetCheckoutKey(account, repo, fingerprint string) (*CheckoutKey, error) { + checkoutKey := &CheckoutKey{} + + err := c.request("GET", fmt.Sprintf("project/%s/%s/checkout-key/%s", account, repo, fingerprint), &checkoutKey, nil, nil) + if err != nil { + return nil, err + } + + return checkoutKey, nil +} + +// DeleteCheckoutKey fetches the checkout key for the given project by fingerprint +func (c *Client) DeleteCheckoutKey(account, repo, fingerprint string) error { + return c.request("DELETE", fmt.Sprintf("project/%s/%s/checkout-key/%s", account, repo, fingerprint), nil, nil, nil) +} + +// AddHerokuKey associates a Heroku key with the user's API token to allow +// CircleCI to deploy to Heroku on your behalf +// +// The API token being used must be a user API token +// +// NOTE: It doesn't look like there is currently a way to dissaccociate your +// Heroku key, so use with care +func (c *Client) AddHerokuKey(key string) error { + body := struct { + APIKey string `json:"apikey"` + }{APIKey: key} + + return c.request("POST", "/user/heroku-key", nil, nil, body) +} + +// EnvVar represents an environment variable +type EnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Artifact represents a build artifact +type Artifact struct { + NodeIndex int `json:"node_index"` + Path string `json:"path"` + PrettyPath string `json:"pretty_path"` + URL string `json:"url"` +} + +// UserProject returns the selective project information included when querying +// for a User +type UserProject struct { + Emails string `json:"emails"` + OnDashboard bool `json:"on_dashboard"` +} + +// User represents a CircleCI user +type User struct { + Admin bool `json:"admin"` + AllEmails []string `json:"all_emails"` + AvatarURL string `json:"avatar_url"` + BasicEmailPrefs string `json:"basic_email_prefs"` + Containers int `json:"containers"` + CreatedAt time.Time `json:"created_at"` + DaysLeftInTrial int `json:"days_left_in_trial"` + GithubID int `json:"github_id"` + GithubOauthScopes []string `json:"github_oauth_scopes"` + GravatarID *string `json:"gravatar_id"` + HerokuAPIKey *string `json:"heroku_api_key"` + LastViewedChangelog time.Time `json:"last_viewed_changelog"` + Login string `json:"login"` + Name *string `json:"name"` + Parallelism int `json:"parallelism"` + Plan *string `json:"plan"` + Projects map[string]*UserProject `json:"projects"` + SelectedEmail *string `json:"selected_email"` + SignInCount int `json:"sign_in_count"` + TrialEnd time.Time `json:"trial_end"` +} + +// AWSConfig represents AWS configuration for a project +type AWSConfig struct { + AWSKeypair *AWSKeypair `json:"keypair"` +} + +// AWSKeypair represents the AWS access/secret key for a project +// SecretAccessKey will be a masked value +type AWSKeypair struct { + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key_id"` +} + +// BuildSummary represents the subset of build information returned with a Project +type BuildSummary struct { + AddedAt time.Time `json:"added_at"` + BuildNum int `json:"build_num"` + Outcome string `json:"outcome"` + PushedAt time.Time `json:"pushed_at"` + Status string `json:"status"` + VCSRevision string `json:"vcs_revision"` +} + +// Branch represents a repository branch +type Branch struct { + LastSuccess *BuildSummary `json:"last_success"` + PusherLogins []string `json:"pusher_logins"` + RecentBuilds []*BuildSummary `json:"recent_builds"` + RunningBuilds []*BuildSummary `json:"running_builds"` +} + +// PublicSSHKey represents the public part of an SSH key associated with a project +// PrivateKey will be a masked value +type PublicSSHKey struct { + Hostname string `json:"hostname"` + PublicKey string `json:"public_key"` + Fingerprint string `json:"fingerprint"` +} + +// Project represents information about a project +type Project struct { + AWSConfig AWSConfig `json:"aws"` + Branches map[string]Branch `json:"branches"` + CampfireNotifyPrefs *string `json:"campfire_notify_prefs"` + CampfireRoom *string `json:"campfire_room"` + CampfireSubdomain *string `json:"campfire_subdomain"` + CampfireToken *string `json:"campfire_token"` + Compile string `json:"compile"` + DefaultBranch string `json:"default_branch"` + Dependencies string `json:"dependencies"` + Extra string `json:"extra"` + FeatureFlags FeatureFlags `json:"feature_flags"` + FlowdockAPIToken *string `json:"flowdock_api_token"` + Followed bool `json:"followed"` + HallNotifyPrefs *string `json:"hall_notify_prefs"` + HallRoomAPIToken *string `json:"hall_room_api_token"` + HasUsableKey bool `json:"has_usable_key"` + HerokuDeployUser *string `json:"heroku_deploy_user"` + HipchatAPIToken *string `json:"hipchat_api_token"` + HipchatNotify *bool `json:"hipchat_notify"` + HipchatNotifyPrefs *string `json:"hipchat_notify_prefs"` + HipchatRoom *string `json:"hipchat_room"` + IrcChannel *string `json:"irc_channel"` + IrcKeyword *string `json:"irc_keyword"` + IrcNotifyPrefs *string `json:"irc_notify_prefs"` + IrcPassword *string `json:"irc_password"` + IrcServer *string `json:"irc_server"` + IrcUsername *string `json:"irc_username"` + Parallel int `json:"parallel"` + Reponame string `json:"reponame"` + Setup string `json:"setup"` + SlackAPIToken *string `json:"slack_api_token"` + SlackChannel *string `json:"slack_channel"` + SlackNotifyPrefs *string `json:"slack_notify_prefs"` + SlackSubdomain *string `json:"slack_subdomain"` + SlackWebhookURL *string `json:"slack_webhook_url"` + SSHKeys []*PublicSSHKey `json:"ssh_keys"` + Test string `json:"test"` + Username string `json:"username"` + VCSURL string `json:"vcs_url"` +} + +type FeatureFlags struct { + TrustyBeta bool `json:"trusty-beta"` + OSX bool `json:"osx"` + SetGithubStatus bool `json:"set-github-status"` + BuildPRsOnly bool `json:"build-prs-only"` + ForksReceiveSecretVars bool `json:"forks-receive-secret-env-vars"` + Fleet *string `json:"fleet"` + BuildForkPRs bool `json:"build-fork-prs"` + AutocancelBuilds bool `json:"autocancel-builds"` + OSS bool `json:"oss"` + MemoryLimit *string `json:"memory-limit"` + + raw map[string]interface{} +} + +func (f *FeatureFlags) UnmarshalJSON(b []byte) error { + if err := json.Unmarshal(b, &f.raw); err != nil { + return err + } + + if v, ok := f.raw["trusty-beta"]; ok { + f.TrustyBeta = v.(bool) + } + + if v, ok := f.raw["osx"]; ok { + f.OSX = v.(bool) + } + + if v, ok := f.raw["set-github-status"]; ok { + f.SetGithubStatus = v.(bool) + } + + if v, ok := f.raw["build-prs-only"]; ok { + f.BuildPRsOnly = v.(bool) + } + + if v, ok := f.raw["forks-receive-secret-env-vars"]; ok { + f.ForksReceiveSecretVars = v.(bool) + } + + if v, ok := f.raw["fleet"]; ok { + if v != nil { + s := v.(string) + f.Fleet = &s + } + } + + if v, ok := f.raw["build-fork-prs"]; ok { + f.BuildForkPRs = v.(bool) + } + + if v, ok := f.raw["autocancel-builds"]; ok { + f.AutocancelBuilds = v.(bool) + } + + if v, ok := f.raw["oss"]; ok { + f.OSS = v.(bool) + } + + if v, ok := f.raw["memory-limit"]; ok { + if v != nil { + s := v.(string) + f.MemoryLimit = &s + } + } + + return nil +} + +// Raw returns the underlying map[string]interface{} representing the feature flags +// This is useful to access flags that have been added to the API, but not yet added to this library +func (f *FeatureFlags) Raw() map[string]interface{} { + return f.raw +} + +// CommitDetails represents information about a commit returned with other +// structs +type CommitDetails struct { + AuthorDate *time.Time `json:"author_date"` + AuthorEmail string `json:"author_email"` + AuthorLogin string `json:"author_login"` + AuthorName string `json:"author_name"` + Body string `json:"body"` + Branch string `json:"branch"` + Commit string `json:"commit"` + CommitURL string `json:"commit_url"` + CommitterDate *time.Time `json:"committer_date"` + CommitterEmail string `json:"committer_email"` + CommitterLogin string `json:"committer_login"` + CommitterName string `json:"committer_name"` + Subject string `json:"subject"` +} + +// Message represents build messages +type Message struct { + Message string `json:"message"` + Type string `json:"type"` +} + +// Node represents the node a build was run on +type Node struct { + ImageID string `json:"image_id"` + Port int `json:"port"` + PublicIPAddr string `json:"public_ip_addr"` + SSHEnabled *bool `json:"ssh_enabled"` + Username string `json:"username"` +} + +// CircleYML represents the serialized CircleCI YML file for a given build +type CircleYML struct { + String string `json:"string"` +} + +// BuildStatus represents status information about the build +// Used when a short summary of previous builds is included +type BuildStatus struct { + BuildTimeMillis int `json:"build_time_millis"` + Status string `json:"status"` + BuildNum int `json:"build_num"` +} + +// BuildUser represents the user that triggered the build +type BuildUser struct { + Email *string `json:"email"` + IsUser bool `json:"is_user"` + Login string `json:"login"` + Name *string `json:"name"` +} + +// Workflow represents the details of the workflow for a build +type Workflow struct { + JobName string `json:"job_name"` + JobId string `json:"job_id"` + UpstreamJobIds []*string `json:"upstream_job_ids"` + WorkflowId string `json:"workflow_id"` + WorkspaceId string `json:"workspace_id"` + WorkflowName string `json:"workflow_name"` +} + +// Build represents the details of a build +type Build struct { + AllCommitDetails []*CommitDetails `json:"all_commit_details"` + AuthorDate *time.Time `json:"author_date"` + AuthorEmail string `json:"author_email"` + AuthorName string `json:"author_name"` + Body string `json:"body"` + Branch string `json:"branch"` + BuildNum int `json:"build_num"` + BuildParameters map[string]string `json:"build_parameters"` + BuildTimeMillis *int `json:"build_time_millis"` + BuildURL string `json:"build_url"` + Canceled bool `json:"canceled"` + CircleYML *CircleYML `json:"circle_yml"` + CommitterDate *time.Time `json:"committer_date"` + CommitterEmail string `json:"committer_email"` + CommitterName string `json:"committer_name"` + Compare *string `json:"compare"` + DontBuild *string `json:"dont_build"` + Failed *bool `json:"failed"` + FeatureFlags map[string]string `json:"feature_flags"` + InfrastructureFail bool `json:"infrastructure_fail"` + IsFirstGreenBuild bool `json:"is_first_green_build"` + JobName *string `json:"job_name"` + Lifecycle string `json:"lifecycle"` + Messages []*Message `json:"messages"` + Node []*Node `json:"node"` + OSS bool `json:"oss"` + Outcome string `json:"outcome"` + Parallel int `json:"parallel"` + Picard *Picard `json:"picard"` + Platform string `json:"platform"` + Previous *BuildStatus `json:"previous"` + PreviousSuccessfulBuild *BuildStatus `json:"previous_successful_build"` + PullRequests []*PullRequest `json:"pull_requests"` + QueuedAt string `json:"queued_at"` + Reponame string `json:"reponame"` + Retries []int `json:"retries"` + RetryOf *int `json:"retry_of"` + SSHEnabled *bool `json:"ssh_enabled"` + SSHUsers []*SSHUser `json:"ssh_users"` + StartTime *time.Time `json:"start_time"` + Status string `json:"status"` + Steps []*Step `json:"steps"` + StopTime *time.Time `json:"stop_time"` + Subject string `json:"subject"` + Timedout bool `json:"timedout"` + UsageQueuedAt string `json:"usage_queued_at"` + User *BuildUser `json:"user"` + Username string `json:"username"` + VcsRevision string `json:"vcs_revision"` + VcsTag string `json:"vcs_tag"` + VCSURL string `json:"vcs_url"` + Workflows *Workflow `json:"workflows"` + Why string `json:"why"` +} + +// Picard represents metadata about an execution environment +type Picard struct { + BuildAgent *BuildAgent `json:"build_agent"` + ResourceClass *ResourceClass `json:"resource_class"` + Executor string `json:"executor"` +} + +// PullRequest represents a pull request +type PullRequest struct { + HeadSha string `json:"head_sha"` + URL string `json:"url"` +} + +// ResourceClass represents usable resource information for a job +type ResourceClass struct { + CPU float64 `json:"cpu"` + RAM int `json:"ram"` + Class string `json:"class"` +} + +// BuildAgent represents an agent's information +type BuildAgent struct { + Image *string `json:"image"` + Properties *BuildAgentProperties `json:"properties"` +} + +// BuildAgentProperties represents agent properties +type BuildAgentProperties struct { + BuildAgent string `json:"image"` + Executor string `json:"executor"` +} + +// Step represents an individual step in a build +// Will contain more than one action if the step was parallelized +type Step struct { + Name string `json:"name"` + Actions []*Action `json:"actions"` +} + +// Action represents an individual action within a build step +type Action struct { + Background bool `json:"background"` + BashCommand *string `json:"bash_command"` + Canceled *bool `json:"canceled"` + Continue *string `json:"continue"` + EndTime *time.Time `json:"end_time"` + ExitCode *int `json:"exit_code"` + Failed *bool `json:"failed"` + HasOutput bool `json:"has_output"` + Index int `json:"index"` + InfrastructureFail *bool `json:"infrastructure_fail"` + Messages []string `json:"messages"` + Name string `json:"name"` + OutputURL string `json:"output_url"` + Parallel bool `json:"parallel"` + RunTimeMillis int `json:"run_time_millis"` + StartTime *time.Time `json:"start_time"` + Status string `json:"status"` + Step int `json:"step"` + Timedout *bool `json:"timedout"` + Truncated bool `json:"truncated"` + Type string `json:"type"` +} + +// TestMetadata represents metadata collected from the test run (e.g. JUnit output) +type TestMetadata struct { + Classname string `json:"classname"` + File string `json:"file"` + Message *string `json:"message"` + Name string `json:"name"` + Result string `json:"result"` + RunTime float64 `json:"run_time"` + Source string `json:"source"` + SourceType string `json:"source_type"` +} + +// Output represents the output of a given action +type Output struct { + Type string `json:"type"` + Time time.Time `json:"time"` + Message string `json:"message"` +} + +// SSHUser represents a user associated with an build with SSH enabled +type SSHUser struct { + GithubID int `json:"github_id"` + Login string `json:"login"` +} + +// CheckoutKey represents an SSH checkout key for a project +type CheckoutKey struct { + PublicKey string `json:"public_key"` + Type string `json:"type"` // github-user-key or deploy-key + Fingerprint string `json:"fingerprint"` + Login *string `json:"login"` // github username if this is a user key + Preferred bool `json:"preferred"` + Time time.Time `json:"time"` // time key was created +} + +// clean up project returned from API by: +// * url decoding branch names (https://discuss.circleci.com/t/api-returns-url-encoded-branch-names-in-json-response/18524/5) +func cleanupProject(project *Project) error { + if project.Branches == nil { + return nil + } + + newBranches := map[string]Branch{} + for name, branch := range project.Branches { + escapedName, err := url.QueryUnescape(name) + if err != nil { + return fmt.Errorf("error url decoding branch name '%s': %s", name, err) + } + + newBranches[escapedName] = branch + } + project.Branches = newBranches + + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d2bab69..d75a3af 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -83,6 +83,8 @@ github.com/howeyc/gopass github.com/inconshreveable/mousetrap # github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af github.com/jmespath/go-jmespath +# github.com/jszwedko/go-circleci v0.3.0 +github.com/jszwedko/go-circleci # github.com/konsorten/go-windows-terminal-sequences v1.0.2 github.com/konsorten/go-windows-terminal-sequences # github.com/mattn/go-colorable v0.1.4