diff --git a/api/cloudcontroller/wrapper/trace_request.go b/api/cloudcontroller/wrapper/trace_request.go new file mode 100644 index 00000000000..73f6fe47bad --- /dev/null +++ b/api/cloudcontroller/wrapper/trace_request.go @@ -0,0 +1,31 @@ +package wrapper + +import ( + "code.cloudfoundry.org/cli/api/cloudcontroller" + "code.cloudfoundry.org/cli/api/shared" +) + +// CCTraceHeaderRequest is a wrapper that adds b3 trace headers to requests. +type CCTraceHeaderRequest struct { + headers *shared.TraceHeaders + connection cloudcontroller.Connection +} + +// NewCCTraceHeaderRequest returns a pointer to a CCTraceHeaderRequest wrapper. +func NewCCTraceHeaderRequest(trace string) *CCTraceHeaderRequest { + return &CCTraceHeaderRequest{ + headers: shared.NewTraceHeaders(trace), + } +} + +// Add tracing headers +func (t *CCTraceHeaderRequest) Make(request *cloudcontroller.Request, passedResponse *cloudcontroller.Response) error { + t.headers.SetHeaders(request.Request) + return t.connection.Make(request, passedResponse) +} + +// Wrap sets the connection in the CCTraceHeaderRequest and returns itself. +func (t *CCTraceHeaderRequest) Wrap(innerconnection cloudcontroller.Connection) cloudcontroller.Connection { + t.connection = innerconnection + return t +} diff --git a/api/cloudcontroller/wrapper/trace_request_test.go b/api/cloudcontroller/wrapper/trace_request_test.go new file mode 100644 index 00000000000..e7a1c5b3b4f --- /dev/null +++ b/api/cloudcontroller/wrapper/trace_request_test.go @@ -0,0 +1,65 @@ +package wrapper_test + +import ( + "bytes" + "net/http" + + "code.cloudfoundry.org/cli/api/cloudcontroller" + "code.cloudfoundry.org/cli/api/cloudcontroller/cloudcontrollerfakes" + . "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CCTraceHeaderRequest", func() { + var ( + fakeConnection *cloudcontrollerfakes.FakeConnection + + wrapper cloudcontroller.Connection + + request *cloudcontroller.Request + response *cloudcontroller.Response + makeErr error + + traceHeader string + ) + + BeforeEach(func() { + fakeConnection = new(cloudcontrollerfakes.FakeConnection) + + traceHeader = "trace-id" + + wrapper = NewCCTraceHeaderRequest(traceHeader).Wrap(fakeConnection) + + body := bytes.NewReader([]byte("foo")) + + req, err := http.NewRequest(http.MethodGet, "https://foo.bar.com/banana", body) + Expect(err).NotTo(HaveOccurred()) + + response = &cloudcontroller.Response{ + RawResponse: []byte("some-response-body"), + HTTPResponse: &http.Response{}, + } + request = cloudcontroller.NewRequest(req, body) + }) + + JustBeforeEach(func() { + makeErr = wrapper.Make(request, response) + }) + + Describe("Make", func() { + It("Adds the request headers", func() { + Expect(makeErr).NotTo(HaveOccurred()) + Expect(request.Header.Get("X-B3-TraceId")).To(Equal(traceHeader)) + Expect(request.Header.Get("X-B3-SpanId")).ToNot(BeEmpty()) + }) + + It("Calls the inner connection", func() { + Expect(fakeConnection.MakeCallCount()).To(Equal(1)) + req, resp := fakeConnection.MakeArgsForCall(0) + Expect(req).To(Equal(request)) + Expect(resp).To(Equal(response)) + }) + }) +}) diff --git a/api/router/wrapper/trace_request.go b/api/router/wrapper/trace_request.go new file mode 100644 index 00000000000..a96a1ce2ee6 --- /dev/null +++ b/api/router/wrapper/trace_request.go @@ -0,0 +1,31 @@ +package wrapper + +import ( + "code.cloudfoundry.org/cli/api/router" + "code.cloudfoundry.org/cli/api/shared" +) + +// RoutingTraceHeaderRequest is a wrapper that adds b3 trace headers to requests. +type RoutingTraceHeaderRequest struct { + headers *shared.TraceHeaders + connection router.Connection +} + +// NewRoutingTraceHeaderRequest returns a pointer to a RoutingTraceHeaderRequest wrapper. +func NewRoutingTraceHeaderRequest(trace string) *RoutingTraceHeaderRequest { + return &RoutingTraceHeaderRequest{ + headers: shared.NewTraceHeaders(trace), + } +} + +// Add tracing headers +func (t *RoutingTraceHeaderRequest) Make(request *router.Request, passedResponse *router.Response) error { + t.headers.SetHeaders(request.Request) + return t.connection.Make(request, passedResponse) +} + +// Wrap sets the connection in the RoutingTraceHeaderRequest and returns itself. +func (t *RoutingTraceHeaderRequest) Wrap(innerconnection router.Connection) router.Connection { + t.connection = innerconnection + return t +} diff --git a/api/router/wrapper/trace_request_test.go b/api/router/wrapper/trace_request_test.go new file mode 100644 index 00000000000..91a420f38ee --- /dev/null +++ b/api/router/wrapper/trace_request_test.go @@ -0,0 +1,64 @@ +package wrapper_test + +import ( + "bytes" + "net/http" + + "code.cloudfoundry.org/cli/api/router" + "code.cloudfoundry.org/cli/api/router/routerfakes" + . "code.cloudfoundry.org/cli/api/router/wrapper" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CCTraceHeaderRequest", func() { + var ( + fakeConnection *routerfakes.FakeConnection + + wrapper router.Connection + + request *router.Request + response *router.Response + makeErr error + + traceHeader string + ) + + BeforeEach(func() { + fakeConnection = new(routerfakes.FakeConnection) + + traceHeader = "trace-id" + wrapper = NewRoutingTraceHeaderRequest(traceHeader).Wrap(fakeConnection) + + body := bytes.NewReader([]byte("foo")) + + req, err := http.NewRequest(http.MethodGet, "https://foo.bar.com/banana", body) + Expect(err).NotTo(HaveOccurred()) + + response = &router.Response{ + RawResponse: []byte("some-response-body"), + HTTPResponse: &http.Response{}, + } + request = router.NewRequest(req, body) + }) + + JustBeforeEach(func() { + makeErr = wrapper.Make(request, response) + }) + + Describe("Make", func() { + It("Adds the request headers", func() { + Expect(makeErr).NotTo(HaveOccurred()) + Expect(request.Header.Get("X-B3-TraceId")).To(Equal(traceHeader)) + Expect(request.Header.Get("X-B3-SpanId")).ToNot(BeEmpty()) + }) + + It("Calls the inner connection", func() { + Expect(fakeConnection.MakeCallCount()).To(Equal(1)) + req, resp := fakeConnection.MakeArgsForCall(0) + Expect(req).To(Equal(request)) + Expect(resp).To(Equal(response)) + }) + }) +}) diff --git a/api/shared/trace_headers.go b/api/shared/trace_headers.go new file mode 100644 index 00000000000..f8a1978bfd1 --- /dev/null +++ b/api/shared/trace_headers.go @@ -0,0 +1,35 @@ +package shared + +import ( + "net/http" + + "code.cloudfoundry.org/cli/util/random" +) + +const ( + B3TraceIDHeader = "X-B3-TraceId" + B3SpanIDHeader = "X-B3-SpanId" +) + +// TraceHeaders sets b3 trace headers to requests. +type TraceHeaders struct { + b3trace string +} + +// NewTraceHeaders returns a pointer to a TraceHeaderRequest. +func NewTraceHeaders(trace string) *TraceHeaders { + return &TraceHeaders{ + b3trace: trace, + } +} + +// Add tracing headers if they are not already set. +func (t *TraceHeaders) SetHeaders(request *http.Request) { + // only override the trace headers if they are not already set (e.g. already explicitly set by cf curl) + if request.Header.Get(B3TraceIDHeader) == "" { + request.Header.Add(B3TraceIDHeader, t.b3trace) + } + if request.Header.Get(B3SpanIDHeader) == "" { + request.Header.Add(B3SpanIDHeader, random.GenerateHex(16)) + } +} diff --git a/api/shared/trace_headers_test.go b/api/shared/trace_headers_test.go new file mode 100644 index 00000000000..660dde0f302 --- /dev/null +++ b/api/shared/trace_headers_test.go @@ -0,0 +1,42 @@ +package shared_test + +import ( + "net/http" + + . "code.cloudfoundry.org/cli/api/shared" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("B3 Trace Headers", func() { + Describe("SetHeaders", func() { + Context("when there are already headers set", func() { + It("does not add the headers", func() { + traceHeaders := NewTraceHeaders("new_trace_id") + request := &http.Request{ + Header: http.Header{}, + } + request.Header.Set("X-B3-TraceId", "old_trace_id") + request.Header.Set("X-B3-SpanId", "old_span_id") + traceHeaders.SetHeaders(request) + + Expect(request.Header.Get("X-B3-TraceId")).To(Equal("old_trace_id")) + Expect(request.Header.Get("X-B3-SpanId")).To(Equal("old_span_id")) + }) + }) + + Context("when there are no headers set", func() { + It("adds the headers", func() { + traceHeaders := NewTraceHeaders("new_trace_id") + request := &http.Request{ + Header: http.Header{}, + } + traceHeaders.SetHeaders(request) + + Expect(request.Header.Get("X-B3-TraceId")).To(Equal("new_trace_id")) + Expect(request.Header.Get("X-B3-SpanId")).ToNot(BeEmpty()) + }) + }) + }) +}) diff --git a/api/uaa/wrapper/trace_request.go b/api/uaa/wrapper/trace_request.go new file mode 100644 index 00000000000..dedebf6e755 --- /dev/null +++ b/api/uaa/wrapper/trace_request.go @@ -0,0 +1,33 @@ +package wrapper + +import ( + "net/http" + + "code.cloudfoundry.org/cli/api/shared" + "code.cloudfoundry.org/cli/api/uaa" +) + +// UAATraceHeaderRequest is a wrapper that adds b3 trace headers to requests. +type UAATraceHeaderRequest struct { + headers *shared.TraceHeaders + connection uaa.Connection +} + +// NewUAATraceHeaderRequest returns a pointer to a UAATraceHeaderRequest wrapper. +func NewUAATraceHeaderRequest(trace string) *UAATraceHeaderRequest { + return &UAATraceHeaderRequest{ + headers: shared.NewTraceHeaders(trace), + } +} + +// Add tracing headers +func (t *UAATraceHeaderRequest) Make(request *http.Request, passedResponse *uaa.Response) error { + t.headers.SetHeaders(request) + return t.connection.Make(request, passedResponse) +} + +// Wrap sets the connection in the UAATraceHeaderRequest and returns itself. +func (t *UAATraceHeaderRequest) Wrap(innerconnection uaa.Connection) uaa.Connection { + t.connection = innerconnection + return t +} diff --git a/api/uaa/wrapper/trace_request_test.go b/api/uaa/wrapper/trace_request_test.go new file mode 100644 index 00000000000..fc978792905 --- /dev/null +++ b/api/uaa/wrapper/trace_request_test.go @@ -0,0 +1,65 @@ +package wrapper_test + +import ( + "bytes" + "net/http" + + "code.cloudfoundry.org/cli/api/uaa" + "code.cloudfoundry.org/cli/api/uaa/uaafakes" + . "code.cloudfoundry.org/cli/api/uaa/wrapper" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CCTraceHeaderRequest", func() { + var ( + fakeConnection *uaafakes.FakeConnection + + wrapper uaa.Connection + + request *http.Request + response *uaa.Response + makeErr error + + traceHeader string + ) + + BeforeEach(func() { + fakeConnection = new(uaafakes.FakeConnection) + + traceHeader = "trace-id" + + wrapper = NewUAATraceHeaderRequest(traceHeader).Wrap(fakeConnection) + + body := bytes.NewReader([]byte("foo")) + + var err error + request, err = http.NewRequest(http.MethodGet, "https://foo.bar.com/banana", body) + Expect(err).NotTo(HaveOccurred()) + + response = &uaa.Response{ + RawResponse: []byte("some-response-body"), + HTTPResponse: &http.Response{}, + } + }) + + JustBeforeEach(func() { + makeErr = wrapper.Make(request, response) + }) + + Describe("Make", func() { + It("Adds the request headers", func() { + Expect(makeErr).NotTo(HaveOccurred()) + Expect(request.Header.Get("X-B3-TraceId")).To(Equal(traceHeader)) + Expect(request.Header.Get("X-B3-SpanId")).ToNot(BeEmpty()) + }) + + It("Calls the inner connection", func() { + Expect(fakeConnection.MakeCallCount()).To(Equal(1)) + req, resp := fakeConnection.MakeArgsForCall(0) + Expect(req).To(Equal(request)) + Expect(resp).To(Equal(response)) + }) + }) +}) diff --git a/command/commandfakes/fake_config.go b/command/commandfakes/fake_config.go index 2b7f1fe4014..1aa3e8e60a3 100644 --- a/command/commandfakes/fake_config.go +++ b/command/commandfakes/fake_config.go @@ -51,6 +51,26 @@ type FakeConfig struct { authorizationEndpointReturnsOnCall map[int]struct { result1 string } + B3SpanIDStub func() string + b3SpanIDMutex sync.RWMutex + b3SpanIDArgsForCall []struct { + } + b3SpanIDReturns struct { + result1 string + } + b3SpanIDReturnsOnCall map[int]struct { + result1 string + } + B3TraceIDStub func() string + b3TraceIDMutex sync.RWMutex + b3TraceIDArgsForCall []struct { + } + b3TraceIDReturns struct { + result1 string + } + b3TraceIDReturnsOnCall map[int]struct { + result1 string + } BinaryNameStub func() string binaryNameMutex sync.RWMutex binaryNameArgsForCall []struct { @@ -867,6 +887,112 @@ func (fake *FakeConfig) AuthorizationEndpointReturnsOnCall(i int, result1 string }{result1} } +func (fake *FakeConfig) B3SpanID() string { + fake.b3SpanIDMutex.Lock() + ret, specificReturn := fake.b3SpanIDReturnsOnCall[len(fake.b3SpanIDArgsForCall)] + fake.b3SpanIDArgsForCall = append(fake.b3SpanIDArgsForCall, struct { + }{}) + stub := fake.B3SpanIDStub + fakeReturns := fake.b3SpanIDReturns + fake.recordInvocation("B3SpanID", []interface{}{}) + fake.b3SpanIDMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeConfig) B3SpanIDCallCount() int { + fake.b3SpanIDMutex.RLock() + defer fake.b3SpanIDMutex.RUnlock() + return len(fake.b3SpanIDArgsForCall) +} + +func (fake *FakeConfig) B3SpanIDCalls(stub func() string) { + fake.b3SpanIDMutex.Lock() + defer fake.b3SpanIDMutex.Unlock() + fake.B3SpanIDStub = stub +} + +func (fake *FakeConfig) B3SpanIDReturns(result1 string) { + fake.b3SpanIDMutex.Lock() + defer fake.b3SpanIDMutex.Unlock() + fake.B3SpanIDStub = nil + fake.b3SpanIDReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeConfig) B3SpanIDReturnsOnCall(i int, result1 string) { + fake.b3SpanIDMutex.Lock() + defer fake.b3SpanIDMutex.Unlock() + fake.B3SpanIDStub = nil + if fake.b3SpanIDReturnsOnCall == nil { + fake.b3SpanIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.b3SpanIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeConfig) B3TraceID() string { + fake.b3TraceIDMutex.Lock() + ret, specificReturn := fake.b3TraceIDReturnsOnCall[len(fake.b3TraceIDArgsForCall)] + fake.b3TraceIDArgsForCall = append(fake.b3TraceIDArgsForCall, struct { + }{}) + stub := fake.B3TraceIDStub + fakeReturns := fake.b3TraceIDReturns + fake.recordInvocation("B3TraceID", []interface{}{}) + fake.b3TraceIDMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeConfig) B3TraceIDCallCount() int { + fake.b3TraceIDMutex.RLock() + defer fake.b3TraceIDMutex.RUnlock() + return len(fake.b3TraceIDArgsForCall) +} + +func (fake *FakeConfig) B3TraceIDCalls(stub func() string) { + fake.b3TraceIDMutex.Lock() + defer fake.b3TraceIDMutex.Unlock() + fake.B3TraceIDStub = stub +} + +func (fake *FakeConfig) B3TraceIDReturns(result1 string) { + fake.b3TraceIDMutex.Lock() + defer fake.b3TraceIDMutex.Unlock() + fake.B3TraceIDStub = nil + fake.b3TraceIDReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeConfig) B3TraceIDReturnsOnCall(i int, result1 string) { + fake.b3TraceIDMutex.Lock() + defer fake.b3TraceIDMutex.Unlock() + fake.B3TraceIDStub = nil + if fake.b3TraceIDReturnsOnCall == nil { + fake.b3TraceIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.b3TraceIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + func (fake *FakeConfig) BinaryName() string { fake.binaryNameMutex.Lock() ret, specificReturn := fake.binaryNameReturnsOnCall[len(fake.binaryNameArgsForCall)] @@ -4028,6 +4154,10 @@ func (fake *FakeConfig) Invocations() map[string][][]interface{} { defer fake.addPluginRepositoryMutex.RUnlock() fake.authorizationEndpointMutex.RLock() defer fake.authorizationEndpointMutex.RUnlock() + fake.b3SpanIDMutex.RLock() + defer fake.b3SpanIDMutex.RUnlock() + fake.b3TraceIDMutex.RLock() + defer fake.b3TraceIDMutex.RUnlock() fake.binaryNameMutex.RLock() defer fake.binaryNameMutex.RUnlock() fake.binaryVersionMutex.RLock() diff --git a/command/config.go b/command/config.go index 676223c7923..bde3b4a754b 100644 --- a/command/config.go +++ b/command/config.go @@ -15,6 +15,7 @@ type Config interface { AddPluginRepository(name string, url string) AuthorizationEndpoint() string APIVersion() string + B3TraceID() string BinaryName() string BinaryVersion() string CFPassword() string diff --git a/command/v7/shared/new_clients.go b/command/v7/shared/new_clients.go index 4486bd8ffab..4654278e9ab 100644 --- a/command/v7/shared/new_clients.go +++ b/command/v7/shared/new_clients.go @@ -47,6 +47,7 @@ func NewWrappedCloudControllerClient(config command.Config, ui command.UI, extra } ccWrappers = append(ccWrappers, extraWrappers...) + ccWrappers = append(ccWrappers, ccWrapper.NewCCTraceHeaderRequest(config.B3TraceID())) ccWrappers = append(ccWrappers, ccWrapper.NewRetryRequest(config.RequestRetryCount())) return ccv3.NewClient(ccv3.Config{ @@ -85,6 +86,7 @@ func newWrappedUAAClient(config command.Config, ui command.UI) (*uaa.Client, err uaaAuthWrapper := uaaWrapper.NewUAAAuthentication(uaaClient, config) uaaClient.WrapConnection(uaaAuthWrapper) + uaaClient.WrapConnection(uaaWrapper.NewUAATraceHeaderRequest(config.B3TraceID())) uaaClient.WrapConnection(uaaWrapper.NewRetryRequest(config.RequestRetryCount())) err = uaaClient.SetupResources(config.UAAEndpoint(), config.AuthorizationEndpoint()) diff --git a/util/configv3/env.go b/util/configv3/env.go index 27e15504fcd..49aea838a40 100644 --- a/util/configv3/env.go +++ b/util/configv3/env.go @@ -5,6 +5,8 @@ import ( "strconv" "strings" "time" + + "code.cloudfoundry.org/cli/util/random" ) // EnvOverride represents all the environment variables read by the CF CLI @@ -20,6 +22,7 @@ type EnvOverride struct { CFStartupTimeout string CFTrace string CFUsername string + CFB3TraceID string DockerPassword string CNBCredentials string Experimental string @@ -160,3 +163,10 @@ func (config *Config) StartupTimeout() time.Duration { return DefaultStartupTimeout } + +func (config *Config) B3TraceID() string { + if config.ENV.CFB3TraceID == "" { + config.ENV.CFB3TraceID = random.GenerateHex(32) + } + return config.ENV.CFB3TraceID +} diff --git a/util/configv3/load_config.go b/util/configv3/load_config.go index 883b2cd31cb..228c4f3f809 100644 --- a/util/configv3/load_config.go +++ b/util/configv3/load_config.go @@ -127,6 +127,7 @@ func LoadConfig(flags ...FlagOverride) (*Config, error) { CFStartupTimeout: os.Getenv("CF_STARTUP_TIMEOUT"), CFTrace: os.Getenv("CF_TRACE"), CFUsername: os.Getenv("CF_USERNAME"), + CFB3TraceID: os.Getenv("CF_B3_TRACE_ID"), DockerPassword: os.Getenv("CF_DOCKER_PASSWORD"), CNBCredentials: os.Getenv("CNB_REGISTRY_CREDS"), Experimental: os.Getenv("CF_CLI_EXPERIMENTAL"), diff --git a/util/random/hex.go b/util/random/hex.go new file mode 100644 index 00000000000..7b9e4cb65bc --- /dev/null +++ b/util/random/hex.go @@ -0,0 +1,16 @@ +package random + +import ( + "crypto/rand" + "encoding/hex" +) + +// GenerateHex returns a random hex string of the given length. +func GenerateHex(length int) string { + b := make([]byte, length/2) + if _, err := rand.Read(b); err != nil { + panic(err) + } + + return hex.EncodeToString(b) +} diff --git a/util/random/hex_test.go b/util/random/hex_test.go new file mode 100644 index 00000000000..cace9c20dc0 --- /dev/null +++ b/util/random/hex_test.go @@ -0,0 +1,13 @@ +package random_test + +import ( + "code.cloudfoundry.org/cli/util/random" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GenerateHex", func() { + It("returns random hex of correct length", func() { + Expect(random.GenerateHex(16)).To(HaveLen(16)) + }) +}) diff --git a/util/random/random_suite_test.go b/util/random/random_suite_test.go new file mode 100644 index 00000000000..f7830ad0f38 --- /dev/null +++ b/util/random/random_suite_test.go @@ -0,0 +1,13 @@ +package random_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRandom(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Random Suite") +}