From 9148e2327ef10a586930f04a3ea1ab599ca86a57 Mon Sep 17 00:00:00 2001 From: Zach Hauser Date: Tue, 21 Jan 2025 14:55:46 -0800 Subject: [PATCH] v2/logging: add support for S3 logstreaming 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 --- v2/logging.go | 72 ++++++++++++++++++++++++++++++++++++++++------ v2/logging_test.go | 59 ++++++++++++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 12 deletions(-) diff --git a/v2/logging.go b/v2/logging.go index 5044972..6cf0c64 100644 --- a/v2/logging.go +++ b/v2/logging.go @@ -20,6 +20,7 @@ const ( LogstreamCriblEndpoint LogstreamEndpointType = "cribl" LogstreamDatadogEndpoint LogstreamEndpointType = "datadog" LogstreamAxiomEndpoint LogstreamEndpointType = "axiom" + LogstreamS3Endpoint LogstreamEndpointType = "s3" ) const ( @@ -27,20 +28,40 @@ const ( 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. @@ -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")) @@ -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) +} diff --git a/v2/logging_test.go b/v2/logging_test.go index f741157..9b4b8ae 100644 --- a/v2/logging_test.go +++ b/v2/logging_test.go @@ -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 @@ -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}) +}