Skip to content

Commit

Permalink
v2/logging: add support for S3 logstreaming
Browse files Browse the repository at this point in the history
We recently added support for S3 logstreaming endpoints to our API. This
involved adding several new fields on the GET LogstreamConfiguration and
PUT LogstreamConfiguration endpoints (and a new destinationType, "s3"),
plus a new AWSExternalID resource and two new endpoints related to it.

This commit updates the Go client library to reflect all of these changes.

Updates tailscale/corp#24533

Signed-off-by: Zach Hauser <zehauser@gmail.com>
  • Loading branch information
zehauser committed Jan 29, 2025
1 parent 8143c7d commit 74c8fc3
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 12 deletions.
72 changes: 64 additions & 8 deletions v2/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,48 @@ const (
LogstreamCriblEndpoint LogstreamEndpointType = "cribl"
LogstreamDatadogEndpoint LogstreamEndpointType = "datadog"
LogstreamAxiomEndpoint LogstreamEndpointType = "axiom"
LogstreamS3Endpoint LogstreamEndpointType = "s3"
)

const (
LogTypeConfig LogType = "configuration"
LogTypeNetwork LogType = "network"
)

const (
S3AccessKeyAuthentication S3AuthenticationType = "accesskey"
S3RoleARNAuthentication S3AuthenticationType = "rolearn"
)

// LogstreamConfiguration type defines a log stream entity in tailscale.
type LogstreamConfiguration struct {
LogType LogType `json:"logType,omitempty"`
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
LogType LogType `json:"logType,omitempty"`
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
S3Bucket string `json:"s3Bucket,omitempty"`
S3Region string `json:"s3Region,omitempty"`
S3KeyPrefix string `json:"s3KeyPrefix,omitempty"`
S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"`
S3AccessKeyID string `json:"s3AccessKeyId,omitempty"`
S3RoleARN string `json:"s3RoleArn,omitempty"`
S3ExternalID string `json:"s3ExternalId,omitempty"`
}

// SetLogstreamConfigurationRequest type defines a request for setting a LogstreamConfiguration.
type SetLogstreamConfigurationRequest struct {
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
Token string `json:"token,omitempty"`
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
Token string `json:"token,omitempty"`
S3Bucket string `json:"s3Bucket,omitempty"`
S3Region string `json:"s3Region,omitempty"`
S3KeyPrefix string `json:"s3KeyPrefix,omitempty"`
S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"`
S3AccessKeyID string `json:"s3AccessKeyId,omitempty"`
S3SecretAccessKey string `json:"s3SecretAccessKey,omitempty"`
S3RoleARN string `json:"s3RoleArn,omitempty"`
S3ExternalID string `json:"s3ExternalId,omitempty"`
}

// LogstreamEndpointType describes the type of the endpoint.
Expand All @@ -49,6 +70,9 @@ type LogstreamEndpointType string
// LogType describes the type of logging.
type LogType string

// S3AuthenticationType describes the type of authentication used to stream logs to a LogstreamS3Endpoint.
type S3AuthenticationType string

// LogstreamConfiguration retrieves the tailnet's [LogstreamConfiguration] for the given [LogType].
func (lr *LoggingResource) LogstreamConfiguration(ctx context.Context, logType LogType) (*LogstreamConfiguration, error) {
req, err := lr.buildRequest(ctx, http.MethodGet, lr.buildTailnetURL("logging", logType, "stream"))
Expand Down Expand Up @@ -78,3 +102,35 @@ func (lr *LoggingResource) DeleteLogstreamConfiguration(ctx context.Context, log

return lr.do(req, nil)
}

// AWSExternalID represents an AWS External ID that Tailscale can use to stream logs from a
// particular Tailscale AWS account to a LogstreamS3Endpoint that uses S3RoleARNAuthentication.
type AWSExternalID struct {
ExternalID string `json:"externalId,omitempty"`
TailscaleAWSAccountID string `json:"tailscaleAwsAccountId,omitempty"`
}

// CreateOrGetAwsExternalId gets an AWS External ID that Tailscale can use to stream logs to
// a LogstreamS3Endpoint using S3RoleARNAuthentication, creating a new one for this tailnet
// when necessary.
func (lr *LoggingResource) CreateOrGetAwsExternalId(ctx context.Context, reusable bool) (*AWSExternalID, error) {
req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id"), requestBody(map[string]bool{
"reusable": reusable,
}))
if err != nil {
return nil, err
}
return body[AWSExternalID](lr, req)
}

// ValidateAWSTrustPolicy validates that Tailscale can assume your AWS IAM role with (and only
// with) the given AWS External ID.
func (lr *LoggingResource) ValidateAWSTrustPolicy(ctx context.Context, awsExternalID string, roleARN string) error {
req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id", awsExternalID, "validate-aws-trust-policy"), requestBody(map[string]string{
"roleArn": roleARN,
}))
if err != nil {
return err
}
return lr.do(req, nil)
}
59 changes: 55 additions & 4 deletions v2/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ func TestClient_SetLogstreamConfiguration(t *testing.T) {
server.ResponseCode = http.StatusOK

logstreamRequest := tsclient.SetLogstreamConfigurationRequest{
DestinationType: tsclient.LogstreamCriblEndpoint,
URL: "http://example.com",
User: "my-user",
Token: "my-token",
DestinationType: tsclient.LogstreamCriblEndpoint,
URL: "http://example.com",
User: "my-user",
Token: "my-token",
S3Bucket: "my-bucket",
S3Region: "us-west-2",
S3KeyPrefix: "logs/",
S3AuthenticationType: tsclient.S3AccessKeyAuthentication,
S3AccessKeyID: "my-access-key-id",
S3SecretAccessKey: "my-secret-access-key",
S3RoleARN: "my-role-arn",
S3ExternalID: "my-external-id",
}
server.ResponseBody = nil

Expand All @@ -64,3 +72,46 @@ func TestClient_DeleteLogstream(t *testing.T) {
assert.Equal(t, http.MethodDelete, server.Method)
assert.Equal(t, "/api/v2/tailnet/example.com/logging/configuration/stream", server.Path)
}

func TestClient_CreateOrGetAwsExternalId(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

wantExternalID := &tsclient.AWSExternalID{
ExternalID: "external-id",
TailscaleAWSAccountID: "account-id",
}
server.ResponseBody = wantExternalID

gotExternalID, err := client.Logging().CreateOrGetAwsExternalId(context.Background(), true)
assert.NoError(t, err)
assert.Equal(t, server.Method, http.MethodPost)
assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id")
assert.Equal(t, gotExternalID, wantExternalID)

gotRequest := make(map[string]bool)
err = json.Unmarshal(server.Body.Bytes(), &gotRequest)
assert.NoError(t, err)
assert.EqualValues(t, gotRequest, map[string]bool{"reusable": true})
}

func TestClient_ValidateAWSTrustPolicy(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

roleARN := "arn:aws:iam::123456789012:role/example-role"

err := client.Logging().ValidateAWSTrustPolicy(context.Background(), "external-id-0000-0000", roleARN)
assert.NoError(t, err)
assert.Equal(t, server.Method, http.MethodPost)
assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id/external-id-0000-0000/validate-aws-trust-policy")

gotRequest := make(map[string]string)
err = json.Unmarshal(server.Body.Bytes(), &gotRequest)
assert.NoError(t, err)
assert.EqualValues(t, gotRequest, map[string]string{"roleArn": roleARN})
}

0 comments on commit 74c8fc3

Please sign in to comment.