Skip to content

Commit

Permalink
feat: gh-app poc
Browse files Browse the repository at this point in the history
  • Loading branch information
plyr4 committed Sep 25, 2024
1 parent 2960513 commit e939def
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 11 deletions.
2 changes: 1 addition & 1 deletion api/build/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func PlanBuild(ctx context.Context, database database.Interface, scm scm.Service
}

// plan all steps for the build
steps, err := step.PlanSteps(ctx, database, scm, p, b)
steps, err := step.PlanSteps(ctx, database, scm, p, b, r)
if err != nil {
// clean up the objects from the pipeline in the database
CleanBuild(ctx, database, b, services, steps, err)
Expand Down
18 changes: 14 additions & 4 deletions api/step/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
// PlanSteps is a helper function to plan all steps
// in the build for execution. This creates the steps
// for the build.
func PlanSteps(ctx context.Context, database database.Interface, scm scm.Service, p *pipeline.Build, b *types.Build) ([]*library.Step, error) {
func PlanSteps(ctx context.Context, database database.Interface, scm scm.Service, p *pipeline.Build, b *types.Build, r *types.Repo) ([]*library.Step, error) {
// variable to store planned steps
steps := []*library.Step{}

Expand All @@ -29,7 +29,7 @@ func PlanSteps(ctx context.Context, database database.Interface, scm scm.Service
// iterate through all steps for each pipeline stage
for _, step := range stage.Steps {
// create the step object
s, err := planStep(ctx, database, scm, b, step, stage.Name)
s, err := planStep(ctx, database, scm, b, r, step, stage.Name)
if err != nil {
return steps, err
}
Expand All @@ -40,7 +40,7 @@ func PlanSteps(ctx context.Context, database database.Interface, scm scm.Service

// iterate through all pipeline steps
for _, step := range p.Steps {
s, err := planStep(ctx, database, scm, b, step, "")
s, err := planStep(ctx, database, scm, b, r, step, "")
if err != nil {
return steps, err
}
Expand All @@ -51,7 +51,7 @@ func PlanSteps(ctx context.Context, database database.Interface, scm scm.Service
return steps, nil
}

func planStep(ctx context.Context, database database.Interface, scm scm.Service, b *types.Build, c *pipeline.Container, stage string) (*library.Step, error) {
func planStep(ctx context.Context, database database.Interface, scm scm.Service, b *types.Build, r *types.Repo, c *pipeline.Container, stage string) (*library.Step, error) {
// create the step object
s := new(library.Step)
s.SetBuildID(b.GetID())
Expand All @@ -64,6 +64,16 @@ func planStep(ctx context.Context, database database.Interface, scm scm.Service,
s.SetReportAs(c.ReportAs)
s.SetCreated(time.Now().UTC().Unix())

if c.ReportStatus {
id, err := scm.CreateChecks(ctx, r, b.GetCommit(), s.GetName(), b.GetEvent())
if err != nil {
// TODO: make this error more meaningful
return nil, err
}

s.SetCheckID(id)
}

// send API call to create the step
s, err := database.CreateStep(ctx, s)
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions api/step/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ func UpdateStep(c *gin.Context) {
return
}

if s.GetCheckID() != 0 {
s.SetReport(input.GetReport())

err = scm.FromContext(c).UpdateChecks(ctx, r, s, b.GetCommit(), b.GetEvent())
if err != nil {
retErr := fmt.Errorf("unable to set step check %s: %w", entry, err)

util.HandleError(c, http.StatusInternalServerError, retErr)

return
}
}

c.JSON(http.StatusOK, s)

// check if the build is in a "final" state
Expand Down
27 changes: 27 additions & 0 deletions api/types/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Repo struct {
PipelineType *string `json:"pipeline_type,omitempty"`
PreviousName *string `json:"previous_name,omitempty"`
ApproveBuild *string `json:"approve_build,omitempty"`
InstallID *int64 `json:"install_id,omitempty"`
}

// Environment returns a list of environment variables
Expand Down Expand Up @@ -345,6 +346,19 @@ func (r *Repo) GetApproveBuild() string {
return *r.ApproveBuild
}

// GetInstallID returns the InstallID field.
//
// When the provided Repo type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (r *Repo) GetInstallID() int64 {
// return zero value if Repo type or InstallID field is nil
if r == nil || r.InstallID == nil {
return 0
}

return *r.InstallID
}

// SetID sets the ID field.
//
// When the provided Repo type is nil, it
Expand Down Expand Up @@ -618,6 +632,19 @@ func (r *Repo) SetApproveBuild(v string) {
r.ApproveBuild = &v
}

// SetInstallID sets the InstallID field.
//
// When the provided Repo type is nil, it
// will set nothing and immediately return.
func (r *Repo) SetInstallID(v int64) {
// return if Repo type is nil
if r == nil {
return
}

r.InstallID = &v
}

// String implements the Stringer interface for the Repo type.
func (r *Repo) String() string {
return fmt.Sprintf(`{
Expand Down
2 changes: 2 additions & 0 deletions cmd/vela-server/scm.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func setupSCM(c *cli.Context, tc *tracing.Client) (scm.Service, error) {
WebUIAddress: c.String("webui-addr"),
Scopes: c.StringSlice("scm.scopes"),
Tracing: tc,
GithubAppID: c.Int64("scm.app.id"),
GithubAppPrivateKey: c.String("scm.app.private_key"),
}

// setup the scm
Expand Down
2 changes: 2 additions & 0 deletions database/repo/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ repos (
pipeline_type TEXT,
previous_name VARCHAR(100),
approve_build VARCHAR(20),
install_id INTEGER,
UNIQUE(full_name)
);
`
Expand Down Expand Up @@ -65,6 +66,7 @@ repos (
pipeline_type TEXT,
previous_name TEXT,
approve_build TEXT,
install_id INTEGER,
UNIQUE(full_name)
);
`
Expand Down
2 changes: 2 additions & 0 deletions database/step/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ steps (
host VARCHAR(250),
runtime VARCHAR(250),
distribution VARCHAR(250),
check_id INTEGER,
report_as VARCHAR(250),
UNIQUE(build_id, number)
);
Expand All @@ -56,6 +57,7 @@ steps (
host TEXT,
runtime TEXT,
distribution TEXT,
check_id INTEGER,
report_as TEXT,
UNIQUE(build_id, number)
);
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/go-vela/server

go 1.23.1

replace github.com/go-vela/types => ../types

require (
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb
github.com/DATA-DOG/go-sqlmock v1.5.2
Expand All @@ -10,6 +12,7 @@ require (
github.com/adhocore/gronx v1.19.0
github.com/alicebob/miniredis/v2 v2.33.0
github.com/aws/aws-sdk-go v1.55.5
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0
github.com/distribution/reference v0.6.0
github.com/drone/envsubst v1.0.3
github.com/ghodss/yaml v1.0.0
Expand All @@ -18,6 +21,7 @@ require (
github.com/go-vela/types v0.25.0-rc1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/go-cmp v0.6.0
github.com/google/go-github/v62 v62.0.0
github.com/google/go-github/v65 v65.0.0
github.com/google/uuid v1.6.0
github.com/goware/urlx v0.3.2
Expand Down Expand Up @@ -85,6 +89,7 @@ require (
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag=
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
Expand Down Expand Up @@ -100,12 +102,12 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-vela/types v0.25.0-rc1 h1:5pCV4pVt1bm6YYUdkNglRDa3PcFX3qGtf5rrmkUvdOc=
github.com/go-vela/types v0.25.0-rc1/go.mod h1:fLv2pbzIy6puAV6Cgh5ixUcchTUHT4D3xX05zIhkA9I=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
Expand All @@ -115,6 +117,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
github.com/google/go-github/v65 v65.0.0 h1:pQ7BmO3DZivvFk92geC0jB0q2m3gyn8vnYPgV7GSLhQ=
github.com/google/go-github/v65 v65.0.0/go.mod h1:DvrqWo5hvsdhJvHd4WyVF9ttANN3BniqjP8uTFMNb60=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
Expand Down
12 changes: 12 additions & 0 deletions scm/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,16 @@ var Flags = []cli.Flag{
"is behind a Firewall or NAT, or when using something like ngrok to forward webhooks. " +
"(defaults to VELA_ADDR).",
},
&cli.Int64Flag{
EnvVars: []string{"VELA_SCM_APP_ID", "SCM_APP_ID"},
FilePath: "/vela/scm/app_id",
Name: "scm.app.id",
Usage: "(optional & experimental) ID for the GitHub App",
},
&cli.StringFlag{
EnvVars: []string{"VELA_SCM_APP_PRIVATE_KEY", "SCM_APP_PRIVATE_KEY"},
FilePath: "/vela/scm/app_private_key",
Name: "scm.app.private_key",
Usage: "(optional & experimental) path to private key for the GitHub App",
},
}
92 changes: 88 additions & 4 deletions scm/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ package github

import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptrace"
"net/url"
"strings"

"github.com/bradleyfalzon/ghinstallation/v2"
api "github.com/go-vela/server/api/types"
"github.com/google/go-github/v62/github"
"github.com/google/go-github/v65/github"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
Expand Down Expand Up @@ -49,13 +57,17 @@ type config struct {
WebUIAddress string
// specifies the OAuth scopes to use for the GitHub client
Scopes []string
// optional and experimental
GithubAppID int64
GithubAppPrivateKey string
}

type client struct {
config *config
OAuth *oauth2.Config
AuthReq *github.AuthorizationRequest
Tracing *tracing.Client
config *config
OAuth *oauth2.Config
AuthReq *github.AuthorizationRequest
Tracing *tracing.Client
AppsTransport *ghinstallation.AppsTransport
// https://pkg.go.dev/github.com/sirupsen/logrus#Entry
Logger *logrus.Entry
}
Expand Down Expand Up @@ -114,6 +126,30 @@ func New(opts ...ClientOpt) (*client, error) {
Scopes: githubScopes,
}

if c.config.GithubAppID != 0 && len(c.config.GithubAppPrivateKey) > 0 {
c.Logger.Infof("sourcing private key from path: %s", c.config.GithubAppPrivateKey)

decodedPEM, err := base64.StdEncoding.DecodeString(c.config.GithubAppPrivateKey)
if err != nil {
return nil, fmt.Errorf("error decoding base64: %w", err)
}

block, _ := pem.Decode(decodedPEM)
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block containing the key")
}

privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
}

transport := ghinstallation.NewAppsTransportFromPrivateKey(http.DefaultTransport, c.config.GithubAppID, privateKey)

transport.BaseURL = c.config.API
c.AppsTransport = transport
}

return c, nil
}

Expand Down Expand Up @@ -179,3 +215,51 @@ func (c *client) newClientToken(ctx context.Context, token string) *github.Clien

return github
}

// helper function to return the GitHub App token.
func (c *client) newGithubAppToken(r *api.Repo) (*github.Client, error) {
// create a github client based off the existing GitHub App configuration
client, err := github.NewClient(&http.Client{Transport: c.AppsTransport}).WithEnterpriseURLs(c.config.API, c.config.API)
if err != nil {
return nil, err
}

// if repo has an install ID, use it to create an installation token
if r.GetInstallID() != 0 {
// create installation token for the repo
t, _, err := client.Apps.CreateInstallationToken(context.Background(), r.GetInstallID(), &github.InstallationTokenOptions{})
if err != nil {
panic(err)
}

return c.newClientToken(t.GetToken()), nil
}

// list all installations (a.k.a. orgs) where the GitHub App is installed
installations, _, err := client.Apps.ListInstallations(context.Background(), &github.ListOptions{})
if err != nil {
return nil, err
}

var id int64
// iterate through the list of installations
for _, install := range installations {
// find the installation that matches the org for the repo
if strings.EqualFold(install.GetAccount().GetLogin(), r.GetOrg()) {
id = install.GetID()
}
}

// failsafe in case the repo does not belong to an org where the GitHub App is installed
if id == 0 {
return nil, err
}

// create installation token for the repo
t, _, err := client.Apps.CreateInstallationToken(context.Background(), id, &github.InstallationTokenOptions{})
if err != nil {
panic(err)
}

return c.newClientToken(t.GetToken()), nil
}
Loading

0 comments on commit e939def

Please sign in to comment.