Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simulate page #455

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions server/assets/js/footer-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(function() {
function addSimulateRedirect(){
const buttonId = "details-to-simulate"
function redirectToSimulate(event) {
const currentPath = location.href
let simulatePath = currentPath.replace("details", "simulate")
document.location = simulatePath
}
document.getElementById(buttonId).addEventListener("click", redirectToSimulate)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be a JS function. We should be able to use a link that we style to look like a button, where the target URL is rendered as part of the page.

}

document.addEventListener('DOMContentLoaded', function() {
addSimulateRedirect()
}, false);


})();
23 changes: 23 additions & 0 deletions server/assets/js/prefill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(function() {
function prefillSimulation(){
function prefillParam(param){
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
if (urlParams.has(param)) {
let paramValue = urlParams.get(param)
document.getElementById(param).value = paramValue
}
}

const BRANCH = "branch"
const USERNAME = "username"

prefillParam(BRANCH)
prefillParam(USERNAME)
Comment on lines +15 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoding specific fields here, it might be nice to make this a bit more generic. One idea is to add a data-queryparam attribute to all of the input tags that need to be pre-filled. Then the JS can use querySelectorAll to find all of them, read the value of of the attribute to get the parameter name, and then do the logic you have here to set the value to the right thing.

}
document.addEventListener('DOMContentLoaded', function() {
prefillSimulation()
}, false);


})();
21 changes: 16 additions & 5 deletions server/handler/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (b *Base) Evaluate(ctx context.Context, installationID int64, trigger commo
return b.RequestReviewsForResult(ctx, prctx, client, trigger, result)
}

func (b *Base) ValidateFetchedConfig(ctx context.Context, prctx pull.Context, client *github.Client, fc FetchedConfig, trigger common.Trigger) (common.Evaluator, error) {
func (b *Base) ValidateFetchedConfig(ctx context.Context, prctx pull.Context, client *github.Client, fc *FetchedConfig, trigger common.Trigger) (common.Evaluator, error) {
logger := zerolog.Ctx(ctx)

switch {
Expand Down Expand Up @@ -233,10 +233,16 @@ func (b *Base) ValidateFetchedConfig(ctx context.Context, prctx pull.Context, cl
return evaluator, nil
}

func (b *Base) EvaluateFetchedConfig(ctx context.Context, prctx pull.Context, client *github.Client, evaluator common.Evaluator, fc FetchedConfig) (common.Result, error) {
func (b *Base) EvaluateConfig(ctx context.Context, prctx pull.Context, client *github.Client, evaluator common.Evaluator, fc *FetchedConfig) common.Result {
result := evaluator.Evaluate(ctx, prctx)

return result
}
Comment on lines +236 to +240
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need this function since it only calls a function on an argument with no other processing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with this approach to contrast with EvaluateFetchedConfig.

This one does the evaluating portion (and any processing if required) while EvaluateFetchedConfig evaluates and posts the results. This way, the simulate page can display what is happening without messing with the status of the PR.


func (b *Base) PostEvaluatedResults(ctx context.Context, prctx pull.Context, client *github.Client, evaluator common.Evaluator, fc *FetchedConfig, result common.Result) (common.Result, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove evaluator as an argument here since it is not used

logger := zerolog.Ctx(ctx)
statusDescription := result.StatusDescription
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this moved up here instead of staying where it was?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It made it easier to understand what was going on IMO


result := evaluator.Evaluate(ctx, prctx)
if result.Error != nil {
msg := fmt.Sprintf("Error evaluating policy in %s: %s", fc.Source, fc.Path)
logger.Warn().Err(result.Error).Msg(msg)
Expand All @@ -245,8 +251,6 @@ func (b *Base) EvaluateFetchedConfig(ctx context.Context, prctx pull.Context, cl
return result, result.Error
}

statusDescription := result.StatusDescription

var statusState string
switch result.Status {
case common.StatusApproved:
Expand All @@ -268,6 +272,13 @@ func (b *Base) EvaluateFetchedConfig(ctx context.Context, prctx pull.Context, cl
return result, nil
}

func (b *Base) EvaluateFetchedConfig(ctx context.Context, prctx pull.Context, client *github.Client, evaluator common.Evaluator, fc *FetchedConfig) (common.Result, error) {
res := b.EvaluateConfig(ctx, prctx, client, evaluator, fc)
var result, err = b.PostEvaluatedResults(ctx, prctx, client, evaluator, fc, res)

return result, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if you're not modifying or using the results, you can return them directly:

return b.PostEvaluatedResults(ctx, prctx, client, evaluator, fc, res)

}

func (b *Base) RequestReviewsForResult(ctx context.Context, prctx pull.Context, client *github.Client, trigger common.Trigger, result common.Result) error {
logger := zerolog.Ctx(ctx)

Expand Down
228 changes: 228 additions & 0 deletions server/handler/base_details.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright 2022 Palantir Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package handler

import (
"context"
"fmt"
"goji.io/pat"
"net/http"
"net/url"
"path"
"strconv"
"strings"

"github.com/alexedwards/scs"
"github.com/bluekeyes/templatetree"
"github.com/google/go-github/v45/github"
"github.com/palantir/go-githubapp/githubapp"
"github.com/palantir/policy-bot/policy/common"
"github.com/palantir/policy-bot/pull"
"github.com/pkg/errors"
)

type DetailsBase struct {
Base
Sessions *scs.Manager
Templates templatetree.HTMLTree
}

type DetailsData struct {
Error error
IsTemporaryError bool
Result *common.Result
PullRequest *github.PullRequest
User string
PolicyURL string
}

func (h *DetailsBase) getURLParams(w http.ResponseWriter, r *http.Request) (string, string, int, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's call this something that indicates the parameters are actually the PR details or PR locator

owner := pat.Param(r, "owner")
repo := pat.Param(r, "repo")
//number :=
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from testing?

number, err := strconv.Atoi(pat.Param(r, "number"))
if err != nil {
http.Error(w, fmt.Sprintf("Invalid pull request number: %v", err), http.StatusBadRequest)
return "", "", -1, err
}

return owner, repo, number, nil
}

func (h *DetailsBase) generatePrContext(ctx context.Context, owner string, repo string, number int) (pull.Context, error) {
installation, err := h.Installations.GetByOwner(ctx, owner)
if err != nil {
if _, notFound := err.(githubapp.InstallationNotFound); notFound {
//h.render404(w, owner, repo, number)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from testing?

return nil, nil
}
return nil, err
}

client, err := h.ClientCreator.NewInstallationClient(installation.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to create github client")
}

v4client, err := h.ClientCreator.NewInstallationV4Client(installation.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to create github client")
}

pr, _, err := client.PullRequests.Get(ctx, owner, repo, number)
if err != nil {
if isNotFound(err) {
//h.render404(w, owner, repo, number)
return nil, nil
}
return nil, errors.Wrap(err, "failed to get pull request")
}
mbrCtx := NewCrossOrgMembershipContext(ctx, client, owner, h.Installations, h.ClientCreator)

prCtx, err := pull.NewGitHubContext(ctx, mbrCtx, client, v4client, pull.Locator{
Owner: owner,
Repo: repo,
Number: number,
Value: pr,
})

return prCtx, err
}

func (h *DetailsBase) getPolicyConfig(ctx context.Context, prCtx pull.Context, branch string) (*FetchedConfig, error) {
owner := prCtx.RepositoryOwner()

installation, err := h.Installations.GetByOwner(ctx, owner)
if err != nil {
if _, notFound := err.(githubapp.InstallationNotFound); notFound {
return nil, errors.Wrap(err, "failed to get github installation")
}
}
client, err := h.ClientCreator.NewInstallationClient(installation.ID)
if err != nil {
err = errors.Wrap(err, "failed to get github installation")
return nil, errors.Wrap(err, "failed to create github client")
}

config := h.ConfigFetcher.ConfigForPRBranch(ctx, prCtx, client, branch)

return config, err
}

func (h *DetailsBase) generateEvaluationDetails(w http.ResponseWriter, r *http.Request, policyConfig *FetchedConfig, prCtx pull.Context) (*DetailsData, *github.Client, common.Evaluator, error) {
ctx := r.Context()
//logger := zerolog.Ctx(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from testing?


owner := prCtx.RepositoryOwner()
repo := prCtx.RepositoryName()
number := prCtx.Number()

installation, err := h.Installations.GetByOwner(ctx, owner)
if err != nil {
if _, notFound := err.(githubapp.InstallationNotFound); notFound {
h.render404(w, owner, repo, number)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a bit weird to have some rendering happen in this function when it's mostly about producing a data object (and a client). I think it would be better to have the caller detect if it is correct to render the 404 page in the same place where it renders the other page.

return nil, nil, nil, errors.Wrap(err, "Unable to find installation")
}
return nil, nil, nil, err
}

client, err := h.ClientCreator.NewInstallationClient(installation.ID)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "failed to create github client")
}

sess := h.Sessions.Load(r)
user, err := sess.GetString(SessionKeyUsername)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "failed to read sessions")
}

level, _, err := client.Repositories.GetPermissionLevel(ctx, owner, repo, user)
if err != nil {
if isNotFound(err) {
h.render404(w, owner, repo, number)
return nil, nil, nil, nil
}
return nil, nil, nil, errors.Wrap(err, "failed to get user permission level")
}

// if the user does not have permission, pretend the repo/PR doesn't exist
if level.GetPermission() == "none" {
h.render404(w, owner, repo, number)
return nil, nil, nil, nil
}

pr, _, err := client.PullRequests.Get(ctx, owner, repo, number)
if err != nil {
if isNotFound(err) {
h.render404(w, owner, repo, number)
return nil, nil, nil, nil
}
return nil, nil, nil, errors.Wrap(err, "failed to get pull request")
}

var data DetailsData

data.PullRequest = pr
data.User = user

data.PolicyURL = getPolicyURL(pr, policyConfig)

evaluator, err := h.Base.ValidateFetchedConfig(ctx, prCtx, client, policyConfig, common.TriggerAll)
if err != nil {
data.Error = err
return &data, nil, nil, nil
}
if evaluator == nil {
data.Error = errors.Errorf("Invalid policy at %s: %s", policyConfig.Source, policyConfig.Path)
return &data, nil, nil, nil
}

return &data, client, evaluator, nil
}

func (h *DetailsBase) render404(w http.ResponseWriter, owner, repo string, number int) {
msg := fmt.Sprintf(
"Not Found: %s/%s#%d\n\nThe repository or pull request does not exist, you do not have permission, or policy-bot is not installed.",
owner, repo, number,
)
http.Error(w, msg, http.StatusNotFound)
}

func (h *DetailsBase) render(w http.ResponseWriter, templateName string, data interface{}) error {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)

//fmt.Println(data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from testing?

return h.Templates.ExecuteTemplate(w, templateName, data)
}

func getPolicyURL(pr *github.PullRequest, config *FetchedConfig) string {
base := pr.GetBase().GetRepo().GetHTMLURL()
if u, _ := url.Parse(base); u != nil {
srcParts := strings.Split(config.Source, "@")
if len(srcParts) != 2 {
return base
}
u.Path = path.Join(srcParts[0], "blob", srcParts[1], config.Path)
return u.String()
}
return base
}

func isNotFound(err error) bool {
rerr, ok := err.(*github.ErrorResponse)
return ok && rerr.Response.StatusCode == http.StatusNotFound
}