diff --git a/client/client.go b/client/client.go index 81f7a25c4..52744ba02 100644 --- a/client/client.go +++ b/client/client.go @@ -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, @@ -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...)) diff --git a/useragent/cicd.go b/useragent/cicd.go new file mode 100644 index 000000000..04ca17677 --- /dev/null +++ b/useragent/cicd.go @@ -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 +} diff --git a/useragent/cicd_test.go b/useragent/cicd_test.go new file mode 100644 index 000000000..6c4898641 --- /dev/null +++ b/useragent/cicd_test.go @@ -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()) +}