diff --git a/x/mongo/driver/operation/hello.go b/x/mongo/driver/operation/hello.go index 16d5809130..de7e05cb5f 100644 --- a/x/mongo/driver/operation/hello.go +++ b/x/mongo/driver/operation/hello.go @@ -153,12 +153,23 @@ const ( envNameVercel = "vercel" ) +const dockerEnvPath = "/.dockerenv" +const envVarK8s = "KUBERNETES_SERVICE_HOST" + +const ( + // Runtime names + runtimeNameDocker = "docker" + + // Orchestrator names + orchestratorNameK8s = "kubernetes" +) + // getFaasEnvName parses the FaaS environment variable name and returns the // corresponding name used by the client. If none of the variables or variables -// for multiple names are populated the client.env value MUST be entirely -// omitted. When variables for multiple "client.env.name" values are present, -// "vercel" takes precedence over "aws.lambda"; any other combination MUST cause -// "client.env" to be entirely omitted. +// for multiple names are populated the FaaS values MUST be entirely omitted. +// When variables for multiple "client.env.name" values are present, "vercel" +// takes precedence over "aws.lambda"; any other combination MUST cause FaaS +// values to be entirely omitted. func getFaasEnvName() string { envVars := []string{ envVarAWSExecutionEnv, @@ -218,6 +229,31 @@ func getFaasEnvName() string { return "" } +type containerInfo struct { + runtime string + orchestrator string +} + +// getContainerEnvInfo returns runtime and orchestrator of a container. +// If no fields is populated, the client.env.container value MUST be entirely +// omitted. +func getContainerEnvInfo() *containerInfo { + var runtime, orchestrator string + if _, err := os.Stat(dockerEnvPath); !os.IsNotExist(err) { + runtime = runtimeNameDocker + } + if v := os.Getenv(envVarK8s); v != "" { + orchestrator = orchestratorNameK8s + } + if runtime != "" || orchestrator != "" { + return &containerInfo{ + runtime: runtime, + orchestrator: orchestrator, + } + } + return nil +} + // appendClientAppName appends the application metadata to the dst. It is the // responsibility of the caller to check that this appending does not cause dst // to exceed any size limitations. @@ -256,14 +292,20 @@ func appendClientEnv(dst []byte, omitNonName, omitDoc bool) ([]byte, error) { } name := getFaasEnvName() - if name == "" { + container := getContainerEnvInfo() + // Omit the entire 'env' if both name and container are empty because other + // fields depend on either of them. + if name == "" && container == nil { return dst, nil } var idx int32 idx, dst = bsoncore.AppendDocumentElementStart(dst, "env") - dst = bsoncore.AppendStringElement(dst, "name", name) + + if name != "" { + dst = bsoncore.AppendStringElement(dst, "name", name) + } addMem := func(envVar string) []byte { mem := os.Getenv(envVar) @@ -306,6 +348,7 @@ func appendClientEnv(dst []byte, omitNonName, omitDoc bool) ([]byte, error) { } if !omitNonName { + // No other FaaS fields will be populated if the name is empty. switch name { case envNameAWSLambda: dst = addMem(envVarAWSLambdaFunctionMemorySize) @@ -319,6 +362,22 @@ func appendClientEnv(dst []byte, omitNonName, omitDoc bool) ([]byte, error) { } } + if container != nil { + var idxCntnr int32 + idxCntnr, dst = bsoncore.AppendDocumentElementStart(dst, "container") + if container.runtime != "" { + dst = bsoncore.AppendStringElement(dst, "runtime", container.runtime) + } + if container.orchestrator != "" { + dst = bsoncore.AppendStringElement(dst, "orchestrator", container.orchestrator) + } + var err error + dst, err = bsoncore.AppendDocumentEnd(dst, idxCntnr) + if err != nil { + return dst, err + } + } + return bsoncore.AppendDocumentEnd(dst, idx) } @@ -358,21 +417,25 @@ func appendClientPlatform(dst []byte) []byte { // name: "" // }, // driver: { -// name: "", -// version: "" +// name: "", +// version: "" // }, // platform: "", // os: { -// type: "", -// name: "", -// architecture: "", -// version: "" +// type: "", +// name: "", +// architecture: "", +// version: "" // }, // env: { -// name: "", -// timeout_sec: 42, -// memory_mb: 1024, -// region: "", +// name: "", +// timeout_sec: 42, +// memory_mb: 1024, +// region: "", +// container: { +// runtime: "", +// orchestrator: "" +// } // } // } func encodeClientMetadata(appname string, maxLen int) ([]byte, error) { diff --git a/x/mongo/driver/operation/hello_test.go b/x/mongo/driver/operation/hello_test.go index 61ba2fde01..b33d7632cd 100644 --- a/x/mongo/driver/operation/hello_test.go +++ b/x/mongo/driver/operation/hello_test.go @@ -272,6 +272,14 @@ func TestAppendClientEnv(t *testing.T) { }, want: []byte(`{"env":{"name":"azure.func"}}`), }, + { + name: "k8s", + env: map[string]string{ + envVarK8s: "0.0.0.0", + }, + want: []byte(`{"env":{"container":{"orchestrator":"kubernetes"}}}`), + }, + // client.env.container.runtime is untested. } for _, test := range tests { @@ -382,11 +390,17 @@ func TestEncodeClientMetadata(t *testing.T) { Architecture string `bson:"architecture,omitempty"` } + type container struct { + Runtime string `bson:"runtime,omitempty"` + Orchestrator string `bson:"orchestrator,omitempty"` + } + type env struct { - Name string `bson:"name,omitempty"` - TimeoutSec int64 `bson:"timeout_sec,omitempty"` - MemoryMB int32 `bson:"memory_mb,omitempty"` - Region string `bson:"region,omitempty"` + Name string `bson:"name,omitempty"` + TimeoutSec int64 `bson:"timeout_sec,omitempty"` + MemoryMB int32 `bson:"memory_mb,omitempty"` + Region string `bson:"region,omitempty"` + Container *container `bson:"container,omitempty"` } type clientMetadata struct { @@ -408,6 +422,7 @@ func TestEncodeClientMetadata(t *testing.T) { t.Setenv(envVarAWSLambdaRuntimeAPI, "lambda") t.Setenv(envVarAWSLambdaFunctionMemorySize, "123") t.Setenv(envVarAWSRegion, "us-east-2") + t.Setenv(envVarK8s, "0.0.0.0") t.Run("nothing is omitted", func(t *testing.T) { got, err := encodeClientMetadata("foo", maxClientMetadataSize) @@ -418,7 +433,14 @@ func TestEncodeClientMetadata(t *testing.T) { Driver: &driver{Name: driverName, Version: version.Driver}, OS: &dist{Type: runtime.GOOS, Architecture: runtime.GOARCH}, Platform: runtime.Version(), - Env: &env{Name: envNameAWSLambda, MemoryMB: 123, Region: "us-east-2"}, + Env: &env{ + Name: envNameAWSLambda, + MemoryMB: 123, + Region: "us-east-2", + Container: &container{ + Orchestrator: "kubernetes", + }, + }, }) assertDocsEqual(t, got, want) @@ -437,7 +459,12 @@ func TestEncodeClientMetadata(t *testing.T) { Driver: &driver{Name: driverName, Version: version.Driver}, OS: &dist{Type: runtime.GOOS, Architecture: runtime.GOARCH}, Platform: runtime.Version(), - Env: &env{Name: envNameAWSLambda}, + Env: &env{ + Name: envNameAWSLambda, + Container: &container{ + Orchestrator: "kubernetes", + }, + }, }) assertDocsEqual(t, got, want) @@ -454,6 +481,10 @@ func TestEncodeClientMetadata(t *testing.T) { // Calculate what the env.name costs. ndst := bsoncore.AppendStringElement(nil, "name", envNameAWSLambda) + idx, ndst := bsoncore.AppendDocumentElementStart(ndst, "container") + ndst = bsoncore.AppendStringElement(ndst, "orchestrator", "kubernetes") + ndst, err = bsoncore.AppendDocumentEnd(ndst, idx) + require.NoError(t, err) // Environment sub name. envSubName := len(edst) - len(ndst) @@ -466,7 +497,12 @@ func TestEncodeClientMetadata(t *testing.T) { Driver: &driver{Name: driverName, Version: version.Driver}, OS: &dist{Type: runtime.GOOS}, Platform: runtime.Version(), - Env: &env{Name: envNameAWSLambda}, + Env: &env{ + Name: envNameAWSLambda, + Container: &container{ + Orchestrator: "kubernetes", + }, + }, }) assertDocsEqual(t, got, want)