Skip to content
This repository was archived by the owner on Sep 6, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# Defines the url for Zappr. Comment it out to load it from ZAPPR_URL environment variable
URL="https://zappr.domain.com"

# Defines the Zappr token. Comment it out to load it from ZAPPR_TOKEN environment variable
Token=""
# Specifies if calls made by Zappr to Github should use its own Client token or yours. Comment it out to load it from ZAPPR_USE_APP_CREDENTIALS environment variable
UseZapprGithubCredentials="true"

[github]
# Defines the github organization
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ You will need to fill in the following values to be able to create a repo:

### Zappr `zappr`

For Zappr you will need a Zappr URL and a Zappr token, the URL is the base URL of your zappr configuration and the token can be found when you login into the UI and copy the value of any request going out the same domain. The cookies that you need are http-only so you can't access them through JS. Just look for a request with the `Cookie:` header and then copy the value.
For Zappr you will need to specify the URL to Zappr, the URL is the base URL of Zappr, its typically `https://zappr.tools-k8s.hellofresh.io`.

### GitHub `github`

Expand Down
49 changes: 29 additions & 20 deletions cmd/repo_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ import (

// CreateRepoOptions are the flags for the create repository command
type CreateRepoOptions struct {
Description string
Private bool
HasPullApprove bool
HasZappr bool
HasTeams bool
HasCollaborators bool
HasLabels bool
HasDefaultLabels bool
HasWebhooks bool
HasBranchProtections bool
HasIssues bool
HasWiki bool
HasPages bool
Description string
Private bool
HasPullApprove bool
HasZappr bool
UseZapprGithubCredentials bool
HasTeams bool
HasCollaborators bool
HasLabels bool
HasDefaultLabels bool
HasWebhooks bool
HasBranchProtections bool
HasIssues bool
HasWiki bool
HasPages bool
}

// NewCreateRepoCmd creates a new create repo command
Expand Down Expand Up @@ -68,6 +69,7 @@ func NewCreateRepoCmd(ctx context.Context) *cobra.Command {
cmd.Flags().BoolVar(&opts.HasDefaultLabels, "rm-default-labels", true, "Removes the default github labels")
cmd.Flags().BoolVar(&opts.HasWebhooks, "has-webhooks", false, "Enables webhooks configurations")
cmd.Flags().BoolVar(&opts.HasBranchProtections, "has-branch-protections", true, "Enables branch protections")
cmd.Flags().BoolVar(&opts.UseZapprGithubCredentials, "use-zappr-credentials", true, "Enables authenticating to Github as Zapps App")

return cmd
}
Expand Down Expand Up @@ -95,6 +97,7 @@ func RunCreateRepo(ctx context.Context, repoName string, opts *CreateRepoOptions
logger.Debugf("\tAdd labels to repository? %s", strconv.FormatBool(opts.HasLabels))
logger.Debugf("\tAdd webhooks to repository? %s", strconv.FormatBool(opts.HasWebhooks))
logger.Debugf("\tConfigure branch protection? %s", strconv.FormatBool(opts.HasBranchProtections))
logger.Debugf("\tAuthenticate to Github as Zappr? %s", strconv.FormatBool(opts.UseZapprGithubCredentials))

description := opts.Description
githubOpts := &repo.GithubRepoOpts{
Expand Down Expand Up @@ -158,17 +161,23 @@ func RunCreateRepo(ctx context.Context, repoName string, opts *CreateRepoOptions
}

var zapprClient zappr.Client
if cfg.Zappr.Token == "" {
logger.Debug("Authenticating to Zappr using Github token")
zapprClient = zappr.NewWithGithubToken(cfg.Zappr.URL, cfg.Github.Token, nil)
} else {
logger.Debug("Authenticating to Zappr using Zappr token")
zapprClient = zappr.NewWithZapprToken(cfg.Zappr.URL, cfg.Zappr.Token, nil)
zapprClient = zappr.New(cfg.Zappr.URL, cfg.Github.Token, nil)

if cfg.Zappr.UseZapprGithubCredentials {
logger.Debug("Retrieving token for zappr github app from zappr")
err = zapprClient.ImpersonateGitHubApp()
if err != nil {
if errwrap.Contains(err, zappr.ErrZapprUnauthorized.Error()) {
return errwrap.Wrapf("could not retrieve token representing github zappr app from zappr. it seems you have not logged in to zappr, if you have, please logout from zappr, log back in and try again: {{err}}", err)
}

return errwrap.Wrapf("could not retrieve token representing github zappr app from zappr: {{err}}", err)
}
}

err = zapprClient.Enable(*ghRepo.ID)
if errwrap.Contains(err, zappr.ErrZapprAlreadyEnabled.Error()) {
logger.Debug("zappr already enabled, moving on...")
logger.Debug("Zappr already enabled, moving on...")
} else if err != nil {
return errwrap.Wrapf("could not enable zappr: {{err}}", err)
}
Expand Down
8 changes: 1 addition & 7 deletions cmd/repo_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,7 @@ func RunDeleteRepo(ctx context.Context, name string, opts *DeleteRepoOpts) error
}

var zapprClient zappr.Client
if cfg.Zappr.Token == "" {
logger.Debug("Authenticating to Zappr using Github token")
zapprClient = zappr.NewWithGithubToken(cfg.Zappr.URL, cfg.Github.Token, nil)
} else {
logger.Debug("Authenticating to Zappr using Zappr token")
zapprClient = zappr.NewWithZapprToken(cfg.Zappr.URL, cfg.Zappr.Token, nil)
}
zapprClient = zappr.New(cfg.Zappr.URL, cfg.Github.Token, nil)

logger.Debug("Disabling Zappr on repo...")
err = zapprClient.Disable(*ghRepo.ID)
Expand Down
7 changes: 6 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ func NewRootCmd() *cobra.Command {
}

cfg := config.WithContext(ctx)

if opts.token != "" {
cfg.Github.Token = opts.token
cfg.GithubTestOrg.Token = opts.token
}

if cfg.Github.Token == "" {
log.WithContext(ctx).Fatal("Github token not specified. Please set the GITHUB_TOKEN environment variable, set it in your config file, or provide it with the \"-t\" flag")
}

if opts.org != "" {
cfg.Github.Organization = opts.org
cfg.GithubTestOrg.Organization = opts.org
Expand All @@ -59,7 +64,7 @@ func NewRootCmd() *cobra.Command {

ctx, err = github.NewContext(ctx, cfg.Github.Token)
if err != nil {
log.WithContext(ctx).WithError(err).Fatal("Could not create the kube client")
log.WithContext(ctx).WithError(err).Fatal("could not create the kube client")
}

// Aggregates Root commands
Expand Down
7 changes: 4 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ type (

// Zappr represents Zappr configurations
Zappr struct {
URL string
Token string
URL string
UseZapprGithubCredentials bool
}

// Github represents the github configurations
Expand Down Expand Up @@ -105,7 +105,8 @@ func NewContext(ctx context.Context, configFile string) (context.Context, error)
viper.SetDefault("githubtestorg.token", os.Getenv("GITHUB_TOKEN"))

viper.SetDefault("zappr.url", os.Getenv("ZAPPR_URL"))
viper.SetDefault("zappr.token", os.Getenv("ZAPPR_TOKEN"))
viper.SetDefault("zappr.usezapprgithubcredentials", true)
viper.BindEnv("ZAPPR_USE_APP_CREDENTIALS", "zappr.usezapprgithubcredentials")

err := viper.ReadInConfig()
if err != nil {
Expand Down
69 changes: 38 additions & 31 deletions pkg/zappr/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import (
type Client interface {
Enable(repoID int) error
Disable(repoID int) error
ImpersonateGitHubApp() error // Retrieves the Github token representing Zappr and uses that for future requests
}

type clientImpl struct {
zapprToken string
githubToken string
slingClient *sling.Sling
githubToken string
githubZapprAppToken string
slingClient *sling.Sling
}

type zapprErrorResponse struct {
Expand All @@ -29,6 +30,10 @@ type zapprErrorResponse struct {
Title string `json:"title,omitempty"`
}

type zapprAppTokenResponse struct {
Token string `json:"token"`
}

var (
timeout = 30 * time.Second

Expand All @@ -45,22 +50,8 @@ var (
ErrZapprServerError = errors.New("unknown error from zappr")
)

// NewWithZapprToken creates a new Zappr client that uses Zappr Token to make calls to Zappr
func NewWithZapprToken(zapprURL string, zapprToken string, httpClient *http.Client) Client {
if httpClient == nil {
httpClient = &http.Client{Timeout: timeout}
}

slingClient := sling.New().Client(httpClient).Base(zapprURL)

return &clientImpl{
zapprToken: zapprToken,
slingClient: slingClient,
}
}

// NewWithGithubToken creates a new Zappr client that uses Github Token to make calls to Zappr
func NewWithGithubToken(zapprURL string, githubToken string, httpClient *http.Client) Client {
// New creates a new Zappr client that uses Github Token to make calls to Zappr
func New(zapprURL string, githubToken string, httpClient *http.Client) Client {
if httpClient == nil {
httpClient = &http.Client{Timeout: timeout}
}
Expand All @@ -75,12 +66,12 @@ func NewWithGithubToken(zapprURL string, githubToken string, httpClient *http.Cl

// Enable turns on Zappr approval check on a Github repo
func (c *clientImpl) Enable(repoID int) error {
req, err := c.slingClient.Get(fmt.Sprintf("api/repos/%d?autoSync=true", repoID)).Request()
req, err := c.slingClient.Get(fmt.Sprintf("/api/repos/%d?autoSync=true", repoID)).Request()
if err != nil {
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
}

status, zapprErrorResponse, err := c.doRequest(req)
status, zapprErrorResponse, err := c.doRequest(req, nil)
if err != nil {
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
}
Expand All @@ -90,7 +81,7 @@ func (c *clientImpl) Enable(repoID int) error {
return errwrap.Wrapf("could not Enable Zappr approval checks on repo: {{err}}", err)
}

status, zapprErrorResponse, err = c.doRequest(req)
status, zapprErrorResponse, err = c.doRequest(req, nil)
if status == http.StatusServiceUnavailable && zapprErrorResponse != nil {
// Zappr already active on the repo
if strings.HasPrefix(zapprErrorResponse.Detail, "Check approval already exists for repository") {
Expand All @@ -103,12 +94,12 @@ func (c *clientImpl) Enable(repoID int) error {

// Disable turns off Zappr approval check on a Github repo
func (c *clientImpl) Disable(repoID int) error {
req, err := c.slingClient.Get(fmt.Sprintf("api/repos/%d?autoSync=true", repoID)).Request()
req, err := c.slingClient.Get(fmt.Sprintf("/api/repos/%d?autoSync=true", repoID)).Request()
if err != nil {
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
}

status, zapprErrorResponse, err := c.doRequest(req)
status, zapprErrorResponse, err := c.doRequest(req, nil)
if err != nil {
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
}
Expand All @@ -118,7 +109,7 @@ func (c *clientImpl) Disable(repoID int) error {
return errwrap.Wrapf("could not Disable Zappr approval checks on repo: {{err}}", err)
}

status, zapprErrorResponse, err = c.doRequest(req)
status, zapprErrorResponse, err = c.doRequest(req, nil)
if status == http.StatusServiceUnavailable && zapprErrorResponse != nil {
// Zappr active on the repo, but repo has been deleted from github
if strings.HasSuffix(zapprErrorResponse.Detail, "required_status_checks 404 Not Found") {
Expand All @@ -134,15 +125,31 @@ func (c *clientImpl) Disable(repoID int) error {
return err
}

func (c *clientImpl) doRequest(req *http.Request) (int, *zapprErrorResponse, error) {
if c.zapprToken == "" {
req.Header.Add("Authorization", fmt.Sprintf("token %s", c.githubToken))
} else {
req.Header.Add("Cookie", c.zapprToken)
func (c *clientImpl) ImpersonateGitHubApp() error {
req, err := c.slingClient.Get("api/apptoken").Request()
if err != nil {
return errwrap.Wrapf("could not fetch github token for zappr github app: {{err}}", err)
}

tokenResponse := &zapprAppTokenResponse{}
_, _, err = c.doRequest(req, tokenResponse)
if err != nil {
return errwrap.Wrapf("could not fetch github token for zappr github app: {{err}}", err)
}

c.githubZapprAppToken = tokenResponse.Token
return nil
}

func (c *clientImpl) doRequest(req *http.Request, response interface{}) (int, *zapprErrorResponse, error) {
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.githubToken))

if c.githubZapprAppToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.githubZapprAppToken))
}

zapprErrorResponse := &zapprErrorResponse{}
resp, err := c.slingClient.Do(req, nil, zapprErrorResponse)
resp, err := c.slingClient.Do(req, response, zapprErrorResponse)

if resp == nil {
// Even though 0 does not seem like a valid http response code
Expand Down
75 changes: 47 additions & 28 deletions pkg/zappr/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zappr

import (
"fmt"
"net/http"
"testing"

Expand All @@ -21,38 +22,12 @@ func TestAuthWithGithubToken(t *testing.T) {
defer testServer.Close()

// Should call the "fetch repo" zappr endpoint to find the just created repo
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", mock.Anything, mock.Anything).Return(test.Response{
Status: http.StatusOK,
})

// Add handlers to define expected call(s) to, and response(s) from Zappr
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getGithubAuthHeader(token), mock.Anything).Return(test.Response{
Status: http.StatusCreated,
})

client.Enable(1)

// Assert expected calls were made to Zappr
zapprMock.AssertExpectations(t)
}

func TestAuthWithZapprToken(t *testing.T) {
token := "abcdefgh"

// Get the Zappr Client, Mock Handler and Test Server
client, zapprMock, testServer := NewMockAndHandlerWithZapprToken(token)

// Start the test server and stop it when done
testServer.Start()
defer testServer.Close()

// Should call the "fetch repo" zappr endpoint to find the just created repo
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", mock.Anything, mock.Anything).Return(test.Response{
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", getGithubAuthHeader(token, false), mock.Anything).Return(test.Response{
Status: http.StatusOK,
})

// Add handlers to define expected call(s) to, and response(s) from Zappr
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getZapprAuthHeader(token), mock.Anything).Return(test.Response{
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getGithubAuthHeader(token, true), mock.Anything).Return(test.Response{
Status: http.StatusCreated,
})

Expand Down Expand Up @@ -295,3 +270,47 @@ func TestProblematicRequest(t *testing.T) {
// Assert expected calls were made to Zappr
zapprMock.AssertExpectations(t)
}

func TestImpersonateGitHubApp(t *testing.T) {
token := "12345678"
zapprAppToken := "abcdefgh"

// Get the Zappr Client, Mock Handler and Test Server
client, zapprMock, testServer := NewMockAndHandlerWithGithubToken(token)

// Start the test server and stop it when done
testServer.Start()
defer testServer.Close()

// Should call the "apptoken" zappr endpoint to get the github token representing zappr app and use the users github token
zapprMock.On("Handle", "GET", "/api/apptoken", getGithubAuthHeader(token, false), mock.Anything).Return(test.Response{
Status: http.StatusOK,
Body: []byte(fmt.Sprintf(`{ "token": "%s" }`, zapprAppToken)),
})

err := client.ImpersonateGitHubApp()

// Assert no errors were received
assert.Nil(t, err)

// Assert expected calls were made to Zappr
zapprMock.AssertExpectations(t)

// reset expectations, i need to use the same mock object that is using the retrieved zappr app github token
zapprMock.ExpectedCalls = []*mock.Call{}

// Should call the "fetch repo" zappr endpoint to find the just created repo and use zappr app's github token
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", getGithubAuthHeader(zapprAppToken, false), mock.Anything).Return(test.Response{
Status: http.StatusOK,
})

// Add handlers to define expected call(s) to, and response(s) from Zappr and use zappr app's github token
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getGithubAuthHeader(zapprAppToken, true), mock.Anything).Return(test.Response{
Status: http.StatusCreated,
})

client.Enable(1)

// Assert expected calls were made to Zappr
zapprMock.AssertExpectations(t)
}
Loading