Skip to content
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
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ deployment records to GitHub's artifact metadata API.
API
5. Failed requests are automatically retried with exponential backoff

## Authentication

Two modes of authentication are supported:

1. Using a [GitHub
App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps#building-a-github-app).
1. Using PAT

> [!NOTE] The provisioned API token or GitHub App must have
> `artifact-metadata: write` with access to all relevant GitHub
> repositories (i.e all GitHub repositories that produces container
> images that are loaded into the cluster).

## Command Line Options

| Flag | Description | Default |
Expand All @@ -45,21 +58,18 @@ deployment records to GitHub's artifact metadata API.

## Environment Variables

| Variable | Description | Default |
|------------------------|---------------------------|------------------------------------------------------|
| `ORG` | GitHub organization name | (required) |
| `BASE_URL` | API base URL | `api.github.com` |
| `DN_TEMPLATE` | Deployment name template | `{{namespace}}/{{deploymentName}}/{{containerName}}` |
| `LOGICAL_ENVIRONMENT` | Logical environment name | (required) |
| `PHYSICAL_ENVIRONMENT` | Physical environment name | `""` |
| `CLUSTER` | Cluster name | (required) |
| `API_TOKEN` | API authentication token | `""` |

> [!NOTE]
> The provisioned API token must have `artifact-metadata: write` with
> access to all relevant GitHub repositories (i.e all GitHub
> repositories that produces container images that are loaded into the
> cluster.
| Variable | Description | Default |
|------------------------|--------------------------------------------|------------------------------------------------------|
| `ORG` | GitHub organization name | (required) |
| `BASE_URL` | API base URL | `api.github.com` |
| `DN_TEMPLATE` | Deployment name template | `{{namespace}}/{{deploymentName}}/{{containerName}}` |
| `LOGICAL_ENVIRONMENT` | Logical environment name | (required) |
| `PHYSICAL_ENVIRONMENT` | Physical environment name | `""` |
| `CLUSTER` | Cluster name | (required) |
| `API_TOKEN` | API authentication token | `""` |
| `GH_APP_ID` | GitHub App ID | `""` |
| `GH_INSTALL_ID` | GitHub App installation ID | `""` |
| `GH_APP_PRIV_KEY` | Path to the private key for the GitHub app | `""` |

### Template Variables

Expand Down
3 changes: 3 additions & 0 deletions cmd/deployment-tracker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ func main() {
Cluster: os.Getenv("CLUSTER"),
APIToken: getEnvOrDefault("API_TOKEN", ""),
BaseURL: getEnvOrDefault("BASE_URL", "api.github.com"),
GHAppID: getEnvOrDefault("GH_APP_ID", ""),
GHInstallID: getEnvOrDefault("GH_INSTALL_ID", ""),
GHAppPrivateKey: getEnvOrDefault("GH_APP_PRIV_KEY", ""),
Organization: os.Getenv("GITHUB_ORG"),
}

Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
Expand All @@ -19,8 +20,11 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-github/v75 v75.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
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/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand All @@ -24,10 +26,17 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
Expand Down Expand Up @@ -115,6 +124,7 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
3 changes: 3 additions & 0 deletions internal/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type Config struct {
Cluster string
APIToken string
BaseURL string
GHAppID string
GHInstallID string
GHAppPrivateKey string
Organization string
}

Expand Down
6 changes: 6 additions & 0 deletions internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func New(clientset kubernetes.Interface, namespace string, cfg *Config) (*Contro
if cfg.APIToken != "" {
clientOpts = append(clientOpts, deploymentrecord.WithAPIToken(cfg.APIToken))
}
if cfg.GHAppID != "" &&
cfg.GHInstallID != "" &&
cfg.GHAppPrivateKey != "" {
clientOpts = append(clientOpts, deploymentrecord.WithGHApp(cfg.GHAppID, cfg.GHInstallID, cfg.GHAppPrivateKey))
}

apiClient, err := deploymentrecord.NewClient(
cfg.BaseURL,
cfg.Organization,
Expand Down
37 changes: 36 additions & 1 deletion pkg/deploymentrecord/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
"math/rand/v2"
"net/http"
"regexp"
"strconv"
"strings"
"time"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/github/deployment-tracker/pkg/metrics"
"golang.org/x/time/rate"
)
Expand All @@ -33,6 +35,7 @@ type Client struct {
httpClient *http.Client
retries int
apiToken string
transport *ghinstallation.Transport
rateLimiter *rate.Limiter
}

Expand Down Expand Up @@ -99,6 +102,30 @@ func WithAPIToken(token string) ClientOption {
}
}

// WithGHApp configures a GitHub app to use for authentication.
// If provided values are invalid, this will panic.
// If an API token is also set, the GitHub App will take precedence.
func WithGHApp(id, installID, pk string) ClientOption {
return func(c *Client) {
pid, err := strconv.Atoi(id)
if err != nil {
panic(err)
}
piid, err := strconv.Atoi(installID)
if err != nil {
panic(err)
}
c.transport, err = ghinstallation.NewKeyFromFile(
http.DefaultTransport,
int64(pid),
int64(piid),
pk)
if err != nil {
panic(err)
}
}
}

// WithRateLimiter sets a custom rate limiter for API calls.
func WithRateLimiter(rps float64, burst int) ClientOption {
return func(c *Client) {
Expand Down Expand Up @@ -171,7 +198,15 @@ func (c *Client) PostOne(ctx context.Context, record *DeploymentRecord) error {
}

req.Header.Set("Content-Type", "application/json")
if c.apiToken != "" {
if c.transport != nil {
// Token is thread safe, so no need for external
// locking
tok, err := c.transport.Token(ctx)
if err != nil {
return fmt.Errorf("failed to get access token: %w", err)
}
req.Header.Set("Authorization", "Bearer "+tok)
} else if c.apiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.apiToken)
}

Expand Down