Skip to content

Commit

Permalink
cogito: add support for Github Enterprise API endpoint
Browse files Browse the repository at this point in the history
Co-authored-by: Nimrod Wandera <wanderanimrod@gmail.com>
Co-authored-by: Jin-Chun <Jin.Chun@ibm.com>
  • Loading branch information
2 people authored and aliculPix4D committed Oct 19, 2023
1 parent 9558fa3 commit 24ab313
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 31 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ With reference to the [GitHub Commit status API], the `POST` parameters (`state`
The log level (one of `debug`, `info`, `warn`, `error`, `silent`).\
Default: `info`.

- `github_api_endpoint`:\
Override the default API endpoint. This allows you to post commit statuses to repositories hosted by GitHub Enterprise (GHE) instances. When provided, this parameter must be a valid https url.
Default: `https://api.github.com`

- `log_url`. **DEPRECATED, no-op, will be removed**\
A Google Hangout Chat webhook. Useful to obtain logging for the `check` step for Concourse < v7.x

Expand Down
11 changes: 2 additions & 9 deletions cmd/cogito/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,14 @@ func mainErr(in io.Reader, out io.Writer, logOut io.Writer, args []string) error
})
log.Info(cogito.BuildInfo())

ghAPI := os.Getenv("COGITO_GITHUB_API")
if ghAPI != "" {
log.Info("endpoint override", "COGITO_GITHUB_API", ghAPI)
} else {
ghAPI = github.API
}

switch cmd {
case "check":
return cogito.Check(log, input, out, args[1:])
case "in":
return cogito.Get(log, input, out, args[1:])
case "out":
putter := cogito.NewPutter(ghAPI, log)
return cogito.Put(log, input, out, args[1:], putter)
putter := cogito.NewPutter(github.API, log)
return cogito.Put(input, out, args[1:], putter)
default:
return fmt.Errorf("cli wiring error; please report")
}
Expand Down
12 changes: 6 additions & 6 deletions cmd/cogito/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ func TestRunPutSuccess(t *testing.T) {
googleChatSpy := testhelp.SpyHttpServer(&chatMsg, chatReply, &gchatUrl, http.StatusOK)
in := bytes.NewReader(testhelp.ToJSON(t, cogito.PutRequest{
Source: cogito.Source{
Owner: "the-owner",
Repo: "the-repo",
AccessToken: "the-secret",
GChatWebHook: googleChatSpy.URL,
LogLevel: "debug",
Owner: "the-owner",
Repo: "the-repo",
AccessToken: "the-secret",
GithubApiEndpoint: gitHubSpy.URL,
GChatWebHook: googleChatSpy.URL,
LogLevel: "debug",
},
Params: cogito.PutParams{State: wantState},
}))
var out bytes.Buffer
var logOut bytes.Buffer
inputDir := testhelp.MakeGitRepoFromTestdata(t, "../../cogito/testdata/one-repo/a-repo",
testhelp.HttpsRemote("the-owner", "the-repo"), "dummySHA", wantGitRef)
t.Setenv("COGITO_GITHUB_API", gitHubSpy.URL)

err := mainErr(in, &out, &logOut, []string{"out", inputDir})

Expand Down
1 change: 1 addition & 0 deletions cogito/ghcommitsink.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (sink GitHubCommitStatusSink) Send() error {
"state", ghState, "owner", sink.Request.Source.Owner,
"repo", sink.Request.Source.Repo, "git-ref", sink.GitRef,
"context", context, "buildURL", buildURL, "description", description)

if err := commitStatus.Add(sink.GitRef, ghState, buildURL, description); err != nil {
return err
}
Expand Down
14 changes: 10 additions & 4 deletions cogito/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"os"
"strings"

Expand Down Expand Up @@ -171,6 +172,7 @@ type Source struct {
ChatAppendSummary bool `json:"chat_append_summary"`
ChatNotifyOnStates []BuildState `json:"chat_notify_on_states"`
Sinks []string `json:"sinks"`
GithubApiEndpoint string `json:"github_api_endpoint"`
}

// String renders Source, redacting the sensitive fields.
Expand All @@ -186,7 +188,8 @@ func (src Source) String() string {
fmt.Fprintf(&bld, "chat_append_summary: %t\n", src.ChatAppendSummary)
fmt.Fprintf(&bld, "chat_notify_on_states: %s\n", src.ChatNotifyOnStates)
// Last one: no newline.
fmt.Fprintf(&bld, "sinks: %s", src.Sinks)
fmt.Fprintf(&bld, "sinks: %s\n", src.Sinks)
fmt.Fprintf(&bld, "github_api_endpoint: %s", src.GithubApiEndpoint)

return bld.String()
}
Expand Down Expand Up @@ -254,10 +257,13 @@ func (src *Source) Validate() error {
return fmt.Errorf("source: missing keys: %s", strings.Join(mandatory, ", "))
}

//
// Validate optional fields.
//
// In this case, nothing to validate.
if src.GithubApiEndpoint != "" {
u, err := url.ParseRequestURI(src.GithubApiEndpoint)
if err != nil || u.Host == "" {
return fmt.Errorf("source: github_api_endpoint '%s' is an invalid api endpoint", src.GithubApiEndpoint)
}
}

//
// Apply defaults.
Expand Down
44 changes: 42 additions & 2 deletions cogito/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ func TestSourceValidationSuccess(t *testing.T) {
return source
},
},
{
name: "optional git source github_api_endpoint",
mkSource: func() cogito.Source {
source := baseGithubSource
source.GithubApiEndpoint = "https://github.coffee.com/api/v3"
return source
},
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -102,6 +110,36 @@ func TestSourceValidationFailure(t *testing.T) {
},
wantErr: "source: invalid sink(s): [closed coffee shop]",
},
{
name: "no protocol prefix in git source github_api_endpoint",
source: cogito.Source{
Owner: "the-owner",
Repo: "the-repo",
AccessToken: "the-token",
GithubApiEndpoint: "github.coffee.com/api/v3",
},
wantErr: "source: github_api_endpoint 'github.coffee.com/api/v3' is an invalid api endpoint",
},
{
name: "invalid http protocol prefix in git source github_api_endpoint",
source: cogito.Source{
Owner: "the-owner",
Repo: "the-repo",
AccessToken: "the-token",
GithubApiEndpoint: "https:github.coffee.com/api/v3",
},
wantErr: "source: github_api_endpoint 'https:github.coffee.com/api/v3' is an invalid api endpoint",
},
{
name: "invalid http protocol prefix in git source github_api_endpoint",
source: cogito.Source{
Owner: "the-owner",
Repo: "the-repo",
AccessToken: "the-token",
GithubApiEndpoint: "john.smith.cim",
},
wantErr: "source: github_api_endpoint 'john.smith.cim' is an invalid api endpoint",
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -192,7 +230,8 @@ log_level: debug
context_prefix: the-prefix
chat_append_summary: true
chat_notify_on_states: [success failure]
sinks: []`
sinks: []
github_api_endpoint: `

have := fmt.Sprint(source)

Expand All @@ -211,7 +250,8 @@ log_level:
context_prefix:
chat_append_summary: false
chat_notify_on_states: []
sinks: []`
sinks: []
github_api_endpoint: `

have := fmt.Sprint(input)

Expand Down
4 changes: 1 addition & 3 deletions cogito/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"fmt"
"io"
"strings"

"github.com/hashicorp/go-hclog"
)

// Putter represents the put step of a Concourse resource.
Expand Down Expand Up @@ -40,7 +38,7 @@ type Sinker interface {
// Additionally, the script may emit metadata as a list of key-value pairs. This data is
// intended for public consumption and will make it upstream, intended to be shown on the
// build's page.
func Put(log hclog.Logger, input []byte, out io.Writer, args []string, putter Putter) error {
func Put(input []byte, out io.Writer, args []string, putter Putter) error {
if err := putter.LoadConfiguration(input, args); err != nil {
return fmt.Errorf("put: %s", err)
}
Expand Down
54 changes: 52 additions & 2 deletions cogito/put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cogito_test
import (
"errors"
"fmt"
"github.com/Pix4D/cogito/github"
"io"
"path/filepath"
"testing"
Expand Down Expand Up @@ -64,7 +65,7 @@ func (ms MockSinker) Send() error {
func TestPutSuccess(t *testing.T) {
putter := MockPutter{sinkers: []cogito.Sinker{MockSinker{}}}

err := cogito.Put(hclog.NewNullLogger(), nil, nil, nil, putter)
err := cogito.Put(nil, nil, nil, putter)

assert.NilError(t, err)
}
Expand All @@ -77,7 +78,7 @@ func TestPutFailure(t *testing.T) {
}

test := func(t *testing.T, tc testCase) {
err := cogito.Put(hclog.NewNullLogger(), nil, nil, nil, tc.putter)
err := cogito.Put(nil, nil, nil, tc.putter)

assert.ErrorContains(t, err, tc.wantErr)
}
Expand Down Expand Up @@ -153,6 +154,22 @@ func TestPutterLoadConfigurationSinksOverrideSuccess(t *testing.T) {
assert.NilError(t, err)
}

func TestPutterLoadConfigurationGhApiEndpointOverrideSuccess(t *testing.T) {
in := []byte(`
{
"source": {
"owner": "the-owner",
"repo": "the-repo",
"access_token": "the-token",
"github_api_endpoint": "https://ghe.company.com"
}
}`)
putter := cogito.NewPutter("dummy-API", hclog.NewNullLogger())
inputDir := []string{""}
err := putter.LoadConfiguration(in, inputDir)
assert.NilError(t, err)
}

func TestPutterLoadConfigurationFailure(t *testing.T) {
type testCase struct {
name string
Expand Down Expand Up @@ -190,6 +207,20 @@ func TestPutterLoadConfigurationFailure(t *testing.T) {
args: []string{},
wantErr: "put: concourse resource protocol violation: missing input directory",
},
{
name: "invalid GH endpoint url in source",
putInput: cogito.PutRequest{
Source: cogito.Source{
Owner: "owner",
Repo: "repo",
AccessToken: "token",
GithubApiEndpoint: "invalid-api-endpoint",
},
Params: cogito.PutParams{State: cogito.StateSuccess},
},
args: []string{},
wantErr: "put: source: github_api_endpoint 'invalid-api-endpoint' is an invalid api endpoint",
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -405,6 +436,25 @@ func TestPutterSinks(t *testing.T) {
assert.Assert(t, ok2)
}

func TestGitHubCommitStatusSinkApiEndpointOverrideFromSource(t *testing.T) {
// default case
defaultApiEndpoint := github.API
defaultPutter := cogito.NewPutter(defaultApiEndpoint, hclog.NewNullLogger())
sinks := defaultPutter.Sinks()
ghSink := sinks[1].(cogito.GitHubCommitStatusSink)
assert.Assert(t, ghSink.GhAPI == defaultApiEndpoint)

// override
customEndpointPutter := cogito.NewPutter(defaultApiEndpoint, hclog.NewNullLogger())
customEndpoint := "https://ghe.company.com"
customEndpointPutter.Request = cogito.PutRequest{
Source: cogito.Source{GithubApiEndpoint: customEndpoint},
}
customPutterSinks := customEndpointPutter.Sinks()
customPutterGhSink := customPutterSinks[1].(cogito.GitHubCommitStatusSink)
assert.Assert(t, customPutterGhSink.GhAPI == customEndpoint)
}

func TestPutterCustomSinks(t *testing.T) {
putter := cogito.NewPutter("dummy-api", hclog.NewNullLogger())
putter.Request = cogito.PutRequest{
Expand Down
22 changes: 17 additions & 5 deletions cogito/putter.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,12 @@ func (putter *ProdPutter) ProcessInputDir() error {
}

func (putter *ProdPutter) Sinks() []Sinker {
source := putter.Request.Source
ghApiEndpoint := getEndpointFromSourceOrDefault(putter, source)
supportedSinkers := map[string]Sinker{
"github": GitHubCommitStatusSink{
Log: putter.log.Named("ghCommitStatus"),
GhAPI: putter.ghAPI,
GhAPI: ghApiEndpoint,
GitRef: putter.gitRef,
Request: putter.Request,
},
Expand All @@ -167,9 +169,9 @@ func (putter *ProdPutter) Sinks() []Sinker {
Request: putter.Request,
},
}
source := putter.Request.Source.Sinks
sourceSinks := source.Sinks
params := putter.Request.Params.Sinks
sinks, _ := MergeAndValidateSinks(source, params)
sinks, _ := MergeAndValidateSinks(sourceSinks, params)

sinkers := make([]Sinker, 0, sinks.Size())
for _, s := range sinks.OrderedList() {
Expand All @@ -179,6 +181,15 @@ func (putter *ProdPutter) Sinks() []Sinker {
return sinkers
}

func getEndpointFromSourceOrDefault(putter *ProdPutter, source Source) string {
if source.GithubApiEndpoint != "" {
return source.GithubApiEndpoint
} else {
// the default
return putter.ghAPI
}
}

func (putter *ProdPutter) Output(out io.Writer) error {
// Following the protocol for put, we return the version and metadata.
// For Cogito, the metadata contains the Concourse build state.
Expand Down Expand Up @@ -262,8 +273,9 @@ func checkGitRepoDir(dir, owner, repo string) error {
if err != nil {
return fmt.Errorf(".git/config: remote: %w", err)
}
left := []string{"github.com", owner, repo}
right := []string{gu.URL.Host, gu.Owner, gu.Repo}

left := []string{owner, repo}
right := []string{gu.Owner, gu.Repo}
for i, l := range left {
r := right[i]
if !strings.EqualFold(l, r) {
Expand Down

0 comments on commit 24ab313

Please sign in to comment.