Skip to content

Commit

Permalink
Add telemetry for CI/CD platform to useragent (#665)
Browse files Browse the repository at this point in the history
## Changes
This PR adds CI/CD provider infromation to the HTTP user agent. This is
useful to track how often and from which CI/CD platforms our tools are
being run from.
 
## Tests
Unit tests and manually

For manual testing I ran a simple script in Go that list's all jobs in a
workspace and inspected the user agent string.
**Case** **github-actions:**
command: 
```
GITHUB_ACTIONS=true go run main.go
```

User Agent:
```
User-Agent: unknown/0.0.0 databricks-sdk-go/0.23.0 go/1.21.0 os/darwin sdk-feature/pagination auth/pat cicd/github
```

**Case** **google-cloud-build:**
command:
```
PROJECT_ID=1 BUILD_ID=2 PROJECT_NUMBER=3 LOCATION=4  go run main.go
```

User agent:
```
User-Agent: unknown/0.0.0 databricks-sdk-go/0.23.0 go/1.21.0 os/darwin sdk-feature/pagination auth/pat cicd/google-cloud-build
```
  • Loading branch information
shreyas-goenka authored Oct 31, 2023
1 parent 0a32a24 commit 2ec4258
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 0 deletions.
14 changes: 14 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,19 @@ func (c *DatabricksClient) addAuthHeaderToUserAgent(r *http.Request) error {
return nil
}

func (c *DatabricksClient) addCiCdProviderToUserAgent(r *http.Request) error {
// Detect if we are running in a CI/CD environment
provider := useragent.CiCdProvider()
if provider == "" {
return nil
}

// Add the detected CI/CD provider to the user agent
ctx := useragent.InContext(r.Context(), "cicd", provider)
*r = *r.WithContext(ctx) // replace request
return nil
}

func (c *DatabricksClient) perform(
ctx context.Context,
method,
Expand All @@ -407,6 +420,7 @@ func (c *DatabricksClient) perform(
c.Config.Authenticate,
c.addHostToRequestUrl,
c.addAuthHeaderToUserAgent,
c.addCiCdProviderToUserAgent,
}, visitors...)
resp, err := retries.Poll(ctx, c.retryTimeout,
c.attempt(ctx, method, requestURL, headers, requestBody, visitors...))
Expand Down
77 changes: 77 additions & 0 deletions useragent/cicd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package useragent

import (
"os"
"sync"
)

type envVar struct {
// Name of the environment variable.
name string

// Expected value of the environment variable. If empty, only the presence
// of the environment variable is checked. If non-empty, the value must
// match exactly.
expectedValue string
}

type cicdProvider struct {
// The name of the CI/CD provider. This is the name included in the user
// agent string.
name string

// The env vars that are expected to be set in the CI/CD provider's runner.
envVars []envVar
}

// List of CI/CD providers and their env vars we can rely on to detect them.
func listCiCdProviders() []cicdProvider {
return []cicdProvider{
{"github", []envVar{{"GITHUB_ACTIONS", "true"}}},
{"gitlab", []envVar{{"GITLAB_CI", "true"}}},
{"jenkins", []envVar{{"JENKINS_URL", ""}}},
{"azure-devops", []envVar{{"TF_BUILD", "True"}}},
{"circle", []envVar{{"CIRCLECI", "true"}}},
{"travis", []envVar{{"TRAVIS", "true"}}},
{"bitbucket", []envVar{{"BITBUCKET_BUILD_NUMBER", ""}}},
{"google-cloud-build", []envVar{{"PROJECT_ID", ""}, {"BUILD_ID", ""}, {"PROJECT_NUMBER", ""}, {"LOCATION", ""}}},
{"aws-code-build", []envVar{{"CODEBUILD_BUILD_ARN", ""}}},
{"tf-cloud", []envVar{{"TFC_RUN_ID", ""}}},
}
}

// detect returns true if all env vars are set and have expected values.
func (p cicdProvider) detect() bool {
for _, envVar := range p.envVars {
v, ok := os.LookupEnv(envVar.name)
if !ok {
return false
}
if envVar.expectedValue != "" && v != envVar.expectedValue {
return false
}
}
return true
}

// lookupCiCdProvider returns the name of the CI/CD provider if detected. Returns the
// first one, if multiple are detected.
func lookupCiCdProvider() string {
for _, p := range listCiCdProviders() {
if p.detect() {
return p.name
}
}
return ""
}

var provider string

var providerOnce sync.Once

func CiCdProvider() string {
providerOnce.Do(func() {
provider = lookupCiCdProvider()
})
return provider
}
123 changes: 123 additions & 0 deletions useragent/cicd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package useragent

import (
"testing"

"github.com/databricks/databricks-sdk-go/internal/env"
"github.com/stretchr/testify/assert"
)

func TestCiCdProviderDetect(t *testing.T) {
cicdProvider := cicdProvider{
name: "foo",
envVars: []envVar{
{"APPLE", "123"},
{"BANANA", "456"},
{"CHERRY", ""},
},
}

// Set some of the env vars.
t.Setenv("APPLE", "123")
t.Setenv("CHERRY", "000")
assert.False(t, cicdProvider.detect(), "should not detect when not all env vars are set")

// Set the rest of the env vars.
t.Setenv("BANANA", "456")
assert.True(t, cicdProvider.detect(), "should detect when all env vars are set")
}

func TestCiCdProviderGithubActions(t *testing.T) {
env.CleanupEnvironment(t)

// No provider detected.
assert.Equal(t, "", lookupCiCdProvider())

// Github Actions detected.
t.Setenv("GITHUB_ACTIONS", "true")
assert.Equal(t, "github", lookupCiCdProvider())
}

func TestCiCdProviderGitlab(t *testing.T) {
env.CleanupEnvironment(t)

// Gitlab detected.
t.Setenv("GITLAB_CI", "true")
assert.Equal(t, "gitlab", lookupCiCdProvider())
}

func TestCiCdProviderJenkins(t *testing.T) {
env.CleanupEnvironment(t)

// Jenkins detected.
t.Setenv("JENKINS_URL", "https://jenkins.example.com")
assert.Equal(t, "jenkins", lookupCiCdProvider())
}

func TestCiCdProviderAzureDevops(t *testing.T) {
env.CleanupEnvironment(t)

// Azure Devops detected.
t.Setenv("TF_BUILD", "True")
assert.Equal(t, "azure-devops", lookupCiCdProvider())
}

func TestCiCdProviderCircle(t *testing.T) {
env.CleanupEnvironment(t)

// Circle detected.
t.Setenv("CIRCLECI", "true")
assert.Equal(t, "circle", lookupCiCdProvider())
}

func TestCiCdProviderTravis(t *testing.T) {
env.CleanupEnvironment(t)

// Travis detected.
t.Setenv("TRAVIS", "true")
assert.Equal(t, "travis", lookupCiCdProvider())
}

func TestCiCdProviderBitbucket(t *testing.T) {
env.CleanupEnvironment(t)

// Bitbucket detected.
t.Setenv("BITBUCKET_BUILD_NUMBER", "123")
assert.Equal(t, "bitbucket", lookupCiCdProvider())
}

func TestCiCdProviderGoogleCloudBuild(t *testing.T) {
env.CleanupEnvironment(t)

// Google Cloud Build detected.
t.Setenv("PROJECT_ID", "foo")
t.Setenv("BUILD_ID", "bar")
t.Setenv("PROJECT_NUMBER", "baz")
t.Setenv("LOCATION", "")
assert.Equal(t, "google-cloud-build", lookupCiCdProvider())
}

func TestCiCdProviderAwsCodeBuild(t *testing.T) {
env.CleanupEnvironment(t)

// AWS Code Build detected.
t.Setenv("CODEBUILD_BUILD_ARN", "arn:aws:codebuild:us-east-1:123456789012:build/my-demo-project:b1e6deae-e4f2-4151-be79-3cc4e82a0bf0")
assert.Equal(t, "aws-code-build", lookupCiCdProvider())
}

func TestCiCdProviderTfCloud(t *testing.T) {
env.CleanupEnvironment(t)

// Terraform Cloud detected.
t.Setenv("TFC_RUN_ID", "run-123")
assert.Equal(t, "tf-cloud", lookupCiCdProvider())
}

func TestCiCdProviderMultiple(t *testing.T) {
env.CleanupEnvironment(t)

// Multiple providers detected. The first one detected is set.
t.Setenv("GITHUB_ACTIONS", "true")
t.Setenv("GITLAB_CI", "true")
assert.Equal(t, "github", lookupCiCdProvider())
}

0 comments on commit 2ec4258

Please sign in to comment.