Skip to content

Commit 74c8fc3

Browse files
committed
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 <[email protected]>
1 parent 8143c7d commit 74c8fc3

File tree

2 files changed

+119
-12
lines changed

2 files changed

+119
-12
lines changed

v2/logging.go

+64-8
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,48 @@ const (
2020
LogstreamCriblEndpoint LogstreamEndpointType = "cribl"
2121
LogstreamDatadogEndpoint LogstreamEndpointType = "datadog"
2222
LogstreamAxiomEndpoint LogstreamEndpointType = "axiom"
23+
LogstreamS3Endpoint LogstreamEndpointType = "s3"
2324
)
2425

2526
const (
2627
LogTypeConfig LogType = "configuration"
2728
LogTypeNetwork LogType = "network"
2829
)
2930

31+
const (
32+
S3AccessKeyAuthentication S3AuthenticationType = "accesskey"
33+
S3RoleARNAuthentication S3AuthenticationType = "rolearn"
34+
)
35+
3036
// LogstreamConfiguration type defines a log stream entity in tailscale.
3137
type LogstreamConfiguration struct {
32-
LogType LogType `json:"logType,omitempty"`
33-
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
34-
URL string `json:"url,omitempty"`
35-
User string `json:"user,omitempty"`
38+
LogType LogType `json:"logType,omitempty"`
39+
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
40+
URL string `json:"url,omitempty"`
41+
User string `json:"user,omitempty"`
42+
S3Bucket string `json:"s3Bucket,omitempty"`
43+
S3Region string `json:"s3Region,omitempty"`
44+
S3KeyPrefix string `json:"s3KeyPrefix,omitempty"`
45+
S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"`
46+
S3AccessKeyID string `json:"s3AccessKeyId,omitempty"`
47+
S3RoleARN string `json:"s3RoleArn,omitempty"`
48+
S3ExternalID string `json:"s3ExternalId,omitempty"`
3649
}
3750

3851
// SetLogstreamConfigurationRequest type defines a request for setting a LogstreamConfiguration.
3952
type SetLogstreamConfigurationRequest struct {
40-
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
41-
URL string `json:"url,omitempty"`
42-
User string `json:"user,omitempty"`
43-
Token string `json:"token,omitempty"`
53+
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
54+
URL string `json:"url,omitempty"`
55+
User string `json:"user,omitempty"`
56+
Token string `json:"token,omitempty"`
57+
S3Bucket string `json:"s3Bucket,omitempty"`
58+
S3Region string `json:"s3Region,omitempty"`
59+
S3KeyPrefix string `json:"s3KeyPrefix,omitempty"`
60+
S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"`
61+
S3AccessKeyID string `json:"s3AccessKeyId,omitempty"`
62+
S3SecretAccessKey string `json:"s3SecretAccessKey,omitempty"`
63+
S3RoleARN string `json:"s3RoleArn,omitempty"`
64+
S3ExternalID string `json:"s3ExternalId,omitempty"`
4465
}
4566

4667
// LogstreamEndpointType describes the type of the endpoint.
@@ -49,6 +70,9 @@ type LogstreamEndpointType string
4970
// LogType describes the type of logging.
5071
type LogType string
5172

73+
// S3AuthenticationType describes the type of authentication used to stream logs to a LogstreamS3Endpoint.
74+
type S3AuthenticationType string
75+
5276
// LogstreamConfiguration retrieves the tailnet's [LogstreamConfiguration] for the given [LogType].
5377
func (lr *LoggingResource) LogstreamConfiguration(ctx context.Context, logType LogType) (*LogstreamConfiguration, error) {
5478
req, err := lr.buildRequest(ctx, http.MethodGet, lr.buildTailnetURL("logging", logType, "stream"))
@@ -78,3 +102,35 @@ func (lr *LoggingResource) DeleteLogstreamConfiguration(ctx context.Context, log
78102

79103
return lr.do(req, nil)
80104
}
105+
106+
// AWSExternalID represents an AWS External ID that Tailscale can use to stream logs from a
107+
// particular Tailscale AWS account to a LogstreamS3Endpoint that uses S3RoleARNAuthentication.
108+
type AWSExternalID struct {
109+
ExternalID string `json:"externalId,omitempty"`
110+
TailscaleAWSAccountID string `json:"tailscaleAwsAccountId,omitempty"`
111+
}
112+
113+
// CreateOrGetAwsExternalId gets an AWS External ID that Tailscale can use to stream logs to
114+
// a LogstreamS3Endpoint using S3RoleARNAuthentication, creating a new one for this tailnet
115+
// when necessary.
116+
func (lr *LoggingResource) CreateOrGetAwsExternalId(ctx context.Context, reusable bool) (*AWSExternalID, error) {
117+
req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id"), requestBody(map[string]bool{
118+
"reusable": reusable,
119+
}))
120+
if err != nil {
121+
return nil, err
122+
}
123+
return body[AWSExternalID](lr, req)
124+
}
125+
126+
// ValidateAWSTrustPolicy validates that Tailscale can assume your AWS IAM role with (and only
127+
// with) the given AWS External ID.
128+
func (lr *LoggingResource) ValidateAWSTrustPolicy(ctx context.Context, awsExternalID string, roleARN string) error {
129+
req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id", awsExternalID, "validate-aws-trust-policy"), requestBody(map[string]string{
130+
"roleArn": roleARN,
131+
}))
132+
if err != nil {
133+
return err
134+
}
135+
return lr.do(req, nil)
136+
}

v2/logging_test.go

+55-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,18 @@ func TestClient_SetLogstreamConfiguration(t *testing.T) {
3636
server.ResponseCode = http.StatusOK
3737

3838
logstreamRequest := tsclient.SetLogstreamConfigurationRequest{
39-
DestinationType: tsclient.LogstreamCriblEndpoint,
40-
URL: "http://example.com",
41-
User: "my-user",
42-
Token: "my-token",
39+
DestinationType: tsclient.LogstreamCriblEndpoint,
40+
URL: "http://example.com",
41+
User: "my-user",
42+
Token: "my-token",
43+
S3Bucket: "my-bucket",
44+
S3Region: "us-west-2",
45+
S3KeyPrefix: "logs/",
46+
S3AuthenticationType: tsclient.S3AccessKeyAuthentication,
47+
S3AccessKeyID: "my-access-key-id",
48+
S3SecretAccessKey: "my-secret-access-key",
49+
S3RoleARN: "my-role-arn",
50+
S3ExternalID: "my-external-id",
4351
}
4452
server.ResponseBody = nil
4553

@@ -64,3 +72,46 @@ func TestClient_DeleteLogstream(t *testing.T) {
6472
assert.Equal(t, http.MethodDelete, server.Method)
6573
assert.Equal(t, "/api/v2/tailnet/example.com/logging/configuration/stream", server.Path)
6674
}
75+
76+
func TestClient_CreateOrGetAwsExternalId(t *testing.T) {
77+
t.Parallel()
78+
79+
client, server := NewTestHarness(t)
80+
server.ResponseCode = http.StatusOK
81+
82+
wantExternalID := &tsclient.AWSExternalID{
83+
ExternalID: "external-id",
84+
TailscaleAWSAccountID: "account-id",
85+
}
86+
server.ResponseBody = wantExternalID
87+
88+
gotExternalID, err := client.Logging().CreateOrGetAwsExternalId(context.Background(), true)
89+
assert.NoError(t, err)
90+
assert.Equal(t, server.Method, http.MethodPost)
91+
assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id")
92+
assert.Equal(t, gotExternalID, wantExternalID)
93+
94+
gotRequest := make(map[string]bool)
95+
err = json.Unmarshal(server.Body.Bytes(), &gotRequest)
96+
assert.NoError(t, err)
97+
assert.EqualValues(t, gotRequest, map[string]bool{"reusable": true})
98+
}
99+
100+
func TestClient_ValidateAWSTrustPolicy(t *testing.T) {
101+
t.Parallel()
102+
103+
client, server := NewTestHarness(t)
104+
server.ResponseCode = http.StatusOK
105+
106+
roleARN := "arn:aws:iam::123456789012:role/example-role"
107+
108+
err := client.Logging().ValidateAWSTrustPolicy(context.Background(), "external-id-0000-0000", roleARN)
109+
assert.NoError(t, err)
110+
assert.Equal(t, server.Method, http.MethodPost)
111+
assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id/external-id-0000-0000/validate-aws-trust-policy")
112+
113+
gotRequest := make(map[string]string)
114+
err = json.Unmarshal(server.Body.Bytes(), &gotRequest)
115+
assert.NoError(t, err)
116+
assert.EqualValues(t, gotRequest, map[string]string{"roleArn": roleARN})
117+
}

0 commit comments

Comments
 (0)