Skip to content
Open
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
62 changes: 31 additions & 31 deletions docs/proto/proto.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
- [AgentConnectStatus.StatusCode](#f5-nginx-agent-sdk-AgentConnectStatus-StatusCode)
- [AgentLogging.Level](#f5-nginx-agent-sdk-AgentLogging-Level)

- [command_svc.proto](#command_svc-proto)
- [Commander](#f5-nginx-agent-sdk-Commander)

- [command.proto](#command-proto)
- [AgentActivityStatus](#f5-nginx-agent-sdk-AgentActivityStatus)
- [ChunkedResourceChunk](#f5-nginx-agent-sdk-ChunkedResourceChunk)
Expand All @@ -39,9 +42,6 @@
- [NginxConfigStatus.Status](#f5-nginx-agent-sdk-NginxConfigStatus-Status)
- [UploadStatus.TransferStatus](#f5-nginx-agent-sdk-UploadStatus-TransferStatus)

- [command_svc.proto](#command_svc-proto)
- [Commander](#f5-nginx-agent-sdk-Commander)

- [common.proto](#common-proto)
- [CertificateDates](#f5-nginx-agent-sdk-CertificateDates)
- [CertificateName](#f5-nginx-agent-sdk-CertificateName)
Expand Down Expand Up @@ -341,6 +341,34 @@ Log level enum



<a name="command_svc-proto"></a>
<p align="right"><a href="#top">Top</a></p>

## command_svc.proto









<a name="f5-nginx-agent-sdk-Commander"></a>

### Commander
Represents a service used to sent command messages between the management server and the agent.

| Method Name | Request Type | Response Type | Description |
| ----------- | ------------ | ------------- | ------------|
| CommandChannel | [Command](#f5-nginx-agent-sdk-Command) stream | [Command](#f5-nginx-agent-sdk-Command) stream | A Bidirectional streaming RPC established by the agent and is kept open |
| Download | [DownloadRequest](#f5-nginx-agent-sdk-DownloadRequest) | [DataChunk](#f5-nginx-agent-sdk-DataChunk) stream | A streaming RPC established by the agent and is used to download resources associated with commands The download stream will be kept open for the duration of the data transfer and will be closed when its done. The transfer is a stream of chunks as follows: header -&gt; data chunk 1 -&gt; data chunk N. Each data chunk is of a size smaller than the maximum gRPC payload |
| Upload | [DataChunk](#f5-nginx-agent-sdk-DataChunk) stream | [UploadStatus](#f5-nginx-agent-sdk-UploadStatus) | A streaming RPC established by the agent and is used to upload resources associated with commands |





<a name="command-proto"></a>
<p align="right"><a href="#top">Top</a></p>

Expand Down Expand Up @@ -652,34 +680,6 @@ Transfer status enum



<a name="command_svc-proto"></a>
<p align="right"><a href="#top">Top</a></p>

## command_svc.proto









<a name="f5-nginx-agent-sdk-Commander"></a>

### Commander
Represents a service used to sent command messages between the management server and the agent.

| Method Name | Request Type | Response Type | Description |
| ----------- | ------------ | ------------- | ------------|
| CommandChannel | [Command](#f5-nginx-agent-sdk-Command) stream | [Command](#f5-nginx-agent-sdk-Command) stream | A Bidirectional streaming RPC established by the agent and is kept open |
| Download | [DownloadRequest](#f5-nginx-agent-sdk-DownloadRequest) | [DataChunk](#f5-nginx-agent-sdk-DataChunk) stream | A streaming RPC established by the agent and is used to download resources associated with commands The download stream will be kept open for the duration of the data transfer and will be closed when its done. The transfer is a stream of chunks as follows: header -&gt; data chunk 1 -&gt; data chunk N. Each data chunk is of a size smaller than the maximum gRPC payload |
| Upload | [DataChunk](#f5-nginx-agent-sdk-DataChunk) stream | [UploadStatus](#f5-nginx-agent-sdk-UploadStatus) | A streaming RPC established by the agent and is used to upload resources associated with commands |





<a name="common-proto"></a>
<p align="right"><a href="#top">Top</a></p>

Expand Down
133 changes: 133 additions & 0 deletions sdk/grpc/meta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Copyright (c) F5, Inc.
*
* This source code is licensed under the Apache License, Version 2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

package grpc

import (
"sync"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func resetMetaForTest() {
meta.ClientId = ""
meta.CloudAccountId = ""
}

func TestInitMeta_PopulatesGlobal(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

InitMeta("client-1", "cloud-1")

assert.Equal(t, "client-1", meta.ClientId)
assert.Equal(t, "cloud-1", meta.CloudAccountId)
}

func TestInitMeta_OverwritesPreviousValues(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

InitMeta("first", "cloud-first")
InitMeta("second", "cloud-second")

assert.Equal(t, "second", meta.ClientId)
assert.Equal(t, "cloud-second", meta.CloudAccountId)
}

func TestNewMessageMeta_UsesGlobalMeta(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

InitMeta("global-client", "global-cloud")
m := NewMessageMeta("msg-123")

require.NotNil(t, m)
assert.Equal(t, "msg-123", m.MessageId)
assert.Equal(t, "global-client", m.ClientId)
assert.Equal(t, "global-cloud", m.CloudAccountId)
require.NotNil(t, m.Timestamp)
}

func TestNewMessageMeta_TimestampIsRecent(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

before := time.Now().Add(-1 * time.Second)
m := NewMessageMeta("msg-time")
after := time.Now().Add(1 * time.Second)

require.NotNil(t, m.Timestamp)
ts := time.Unix(m.Timestamp.Seconds, int64(m.Timestamp.Nanos))
assert.True(t, ts.After(before), "timestamp should be after %v, got %v", before, ts)
assert.True(t, ts.Before(after), "timestamp should be before %v, got %v", after, ts)
}

func TestNewMessageMeta_BeforeInitMeta(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

m := NewMessageMeta("msg-no-init")
require.NotNil(t, m)
assert.Empty(t, m.ClientId)
assert.Empty(t, m.CloudAccountId)
assert.Equal(t, "msg-no-init", m.MessageId)
}

func TestNewMeta_SetsAllFields(t *testing.T) {
m := NewMeta("client-x", "msg-x", "cloud-x")

require.NotNil(t, m)
assert.Equal(t, "client-x", m.ClientId)
assert.Equal(t, "msg-x", m.MessageId)
assert.Equal(t, "cloud-x", m.CloudAccountId)
require.NotNil(t, m.Timestamp)
}

func TestNewMeta_DoesNotMutateGlobal(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

InitMeta("global", "global-cloud")
_ = NewMeta("other-client", "msg", "other-cloud")

assert.Equal(t, "global", meta.ClientId)
assert.Equal(t, "global-cloud", meta.CloudAccountId)
}

func TestNewMeta_EmptyArgs(t *testing.T) {
m := NewMeta("", "", "")
require.NotNil(t, m)
assert.Empty(t, m.ClientId)
assert.Empty(t, m.MessageId)
assert.Empty(t, m.CloudAccountId)
assert.NotNil(t, m.Timestamp)
}

func TestNewMessageMeta_ConcurrentReads(t *testing.T) {
resetMetaForTest()
t.Cleanup(resetMetaForTest)

InitMeta("concurrent-client", "concurrent-cloud")

const goroutines = 50
var wg sync.WaitGroup
wg.Add(goroutines)

for range goroutines {
go func() {
defer wg.Done()
m := NewMessageMeta("msg")
assert.Equal(t, "concurrent-client", m.ClientId)
assert.Equal(t, "concurrent-cloud", m.CloudAccountId)
}()
}
wg.Wait()
}
166 changes: 166 additions & 0 deletions sdk/interceptors/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Copyright (c) F5, Inc.
*
* This source code is licensed under the Apache License, Version 2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

package interceptors

import (
"context"
"errors"
"testing"

log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

const (
testUUID = "test-uuid-1234"
testToken = "test-token-abcd"
testBearer = "test-bearer-xyz"
)

func TestNewClientAuth_DefaultLogger(t *testing.T) {
ci := NewClientAuth(testUUID, testToken)

require.NotNil(t, ci)
assert.Equal(t, testUUID, ci.uuid)
assert.Equal(t, testToken, ci.token)
assert.Empty(t, ci.bearer)
assert.NotNil(t, ci.log, "default logger should be assigned")
}

func TestNewClientAuth_WithBearerToken(t *testing.T) {
ci := NewClientAuth(testUUID, testToken, WithBearerToken(testBearer))

require.NotNil(t, ci)
assert.Equal(t, testBearer, ci.bearer)
}

func TestNewClientAuth_MultipleOptions(t *testing.T) {
// Applying option multiple times: last one wins.
ci := NewClientAuth(testUUID, testToken,
WithBearerToken("first"),
WithBearerToken("second"),
)

require.NotNil(t, ci)
assert.Equal(t, "second", ci.bearer)
}

func TestClientInterceptor_GetRequestMetadata(t *testing.T) {
ci := NewClientAuth(testUUID, testToken, WithBearerToken(testBearer))

md, err := ci.GetRequestMetadata(context.Background())
require.NoError(t, err)

assert.Equal(t, testToken, md[TokenHeader])
assert.Equal(t, testUUID, md[IDHeader])
assert.Equal(t, testBearer, md[BearerHeader])
assert.Len(t, md, 3)
}

func TestClientInterceptor_GetRequestMetadata_IgnoresURIArgs(t *testing.T) {
ci := NewClientAuth(testUUID, testToken)

md, err := ci.GetRequestMetadata(context.Background(), "ignored", "args")
require.NoError(t, err)
assert.Equal(t, testToken, md[TokenHeader])
}

func TestClientInterceptor_RequireTransportSecurity_ReturnsFalse(t *testing.T) {
ci := NewClientAuth(testUUID, testToken)
assert.False(t, ci.RequireTransportSecurity())
}

func TestClientInterceptor_Unary_AttachesMetadata(t *testing.T) {
ci := NewClientAuth(testUUID, testToken, WithBearerToken(testBearer))
unary := ci.Unary()
require.NotNil(t, unary)

var capturedCtx context.Context
invoker := func(ctx context.Context, _ string, _, _ interface{},
_ *grpc.ClientConn, _ ...grpc.CallOption,
) error {
capturedCtx = ctx
return nil
}

err := unary(context.Background(), "/svc/Method", nil, nil, nil, invoker)
require.NoError(t, err)

md, ok := metadata.FromOutgoingContext(capturedCtx)
require.True(t, ok, "outgoing metadata should be present")
assert.Equal(t, []string{testToken}, md.Get(TokenHeader))
assert.Equal(t, []string{testUUID}, md.Get(IDHeader))
assert.Equal(t, []string{testBearer}, md.Get(BearerHeader))
}

func TestClientInterceptor_Unary_PropagatesInvokerError(t *testing.T) {
ci := NewClientAuth(testUUID, testToken)
wantErr := errors.New("invoker exploded")

invoker := func(_ context.Context, _ string, _, _ interface{},
_ *grpc.ClientConn, _ ...grpc.CallOption,
) error {
return wantErr
}

err := ci.Unary()(context.Background(), "/svc/Method", nil, nil, nil, invoker)
assert.ErrorIs(t, err, wantErr)
}

func TestClientInterceptor_Stream_AttachesMetadata(t *testing.T) {
ci := NewClientAuth(testUUID, testToken, WithBearerToken(testBearer))
stream := ci.Stream()
require.NotNil(t, stream)

var capturedCtx context.Context
streamer := func(ctx context.Context, _ *grpc.StreamDesc, _ *grpc.ClientConn,
_ string, _ ...grpc.CallOption,
) (grpc.ClientStream, error) {
capturedCtx = ctx
return nil, nil
}

_, err := stream(context.Background(), &grpc.StreamDesc{}, nil, "/svc/Stream", streamer)
require.NoError(t, err)

md, ok := metadata.FromOutgoingContext(capturedCtx)
require.True(t, ok)
assert.Equal(t, []string{testToken}, md.Get(TokenHeader))
assert.Equal(t, []string{testUUID}, md.Get(IDHeader))
assert.Equal(t, []string{testBearer}, md.Get(BearerHeader))
}

func TestClientInterceptor_AttachToken_PreservesExistingMetadata(t *testing.T) {
ci := NewClientAuth(testUUID, testToken)

existing := metadata.Pairs("custom-key", "custom-value")
ctx := metadata.NewOutgoingContext(context.Background(), existing)

out := ci.attachToken(ctx)
md, ok := metadata.FromOutgoingContext(out)
require.True(t, ok)

assert.Equal(t, []string{"custom-value"}, md.Get("custom-key"))
assert.Equal(t, []string{testToken}, md.Get(TokenHeader))
}

func TestClientInterceptor_CustomLogger(t *testing.T) {
customLogger := log.New()
ci := NewClientAuth(testUUID, testToken, withLoggerForTest(customLogger))

assert.Same(t, customLogger, ci.log)
}

func withLoggerForTest(l *log.Logger) Option {
return func(opt *option) {
opt.client.log = l
}
}
Loading
Loading