Skip to content

Commit 3d5eccc

Browse files
authored
Add stackdriver project sink support (#432)
* Vendor cloud logging api * Add logging sink support * Remove typo * Set Filter simpler * Rename typ, typName to resourceType, resourceId * Handle notFoundError * Use # instead of // for hcl comments * Cleanup test code * Change testAccCheckLoggingProjectSink to take a provided api object * Fix whitespace change after merge conflict
1 parent b694d5a commit 3d5eccc

11 files changed

+15288
-0
lines changed

google/config.go

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"google.golang.org/api/container/v1"
2626
"google.golang.org/api/dns/v1"
2727
"google.golang.org/api/iam/v1"
28+
cloudlogging "google.golang.org/api/logging/v2"
2829
"google.golang.org/api/pubsub/v1"
2930
"google.golang.org/api/runtimeconfig/v1beta1"
3031
"google.golang.org/api/servicemanagement/v1"
@@ -46,6 +47,7 @@ type Config struct {
4647
clientComputeBeta *computeBeta.Service
4748
clientContainer *container.Service
4849
clientDns *dns.Service
50+
clientLogging *cloudlogging.Service
4951
clientPubsub *pubsub.Service
5052
clientResourceManager *cloudresourcemanager.Service
5153
clientResourceManagerV2Beta1 *resourceManagerV2Beta1.Service
@@ -153,6 +155,13 @@ func (c *Config) loadAndValidate() error {
153155
}
154156
c.clientDns.UserAgent = userAgent
155157

158+
log.Printf("[INFO] Instantiating Google Stackdriver Logging client...")
159+
c.clientLogging, err = cloudlogging.New(client)
160+
if err != nil {
161+
return err
162+
}
163+
c.clientLogging.UserAgent = userAgent
164+
156165
log.Printf("[INFO] Instantiating Google Storage Client...")
157166
c.clientStorage, err = storage.New(client)
158167
if err != nil {

google/logging_utils.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
)
7+
8+
// loggingSinkResourceTypes contains all the possible Stackdriver Logging resource types. Used to parse ids safely.
9+
var loggingSinkResourceTypes = []string{
10+
"billingAccount",
11+
"folders",
12+
"organizations",
13+
"projects",
14+
}
15+
16+
// LoggingSinkId represents the parts that make up the canonical id used within terraform for a logging resource.
17+
type LoggingSinkId struct {
18+
resourceType string
19+
resourceId string
20+
name string
21+
}
22+
23+
// loggingSinkIdRegex matches valid logging sink canonical ids
24+
var loggingSinkIdRegex = regexp.MustCompile("(.+)/(.+)/sinks/(.+)")
25+
26+
// canonicalId returns the LoggingSinkId as the canonical id used within terraform.
27+
func (l LoggingSinkId) canonicalId() string {
28+
return fmt.Sprintf("%s/%s/sinks/%s", l.resourceType, l.resourceId, l.name)
29+
}
30+
31+
// parent returns the "parent-level" resource that the sink is in (e.g. `folders/foo` for id `folders/foo/sinks/bar`)
32+
func (l LoggingSinkId) parent() string {
33+
return fmt.Sprintf("%s/%s", l.resourceType, l.resourceId)
34+
}
35+
36+
// parseLoggingSinkId parses a canonical id into a LoggingSinkId, or returns an error on failure.
37+
func parseLoggingSinkId(id string) (*LoggingSinkId, error) {
38+
parts := loggingSinkIdRegex.FindStringSubmatch(id)
39+
if parts == nil {
40+
return nil, fmt.Errorf("unable to parse logging sink id %#v", id)
41+
}
42+
// If our resourceType is not a valid logging sink resource type, complain loudly
43+
validLoggingSinkResourceType := false
44+
for _, v := range loggingSinkResourceTypes {
45+
if v == parts[1] {
46+
validLoggingSinkResourceType = true
47+
break
48+
}
49+
}
50+
51+
if !validLoggingSinkResourceType {
52+
return nil, fmt.Errorf("Logging resource type %s is not valid. Valid resource types: %#v", parts[1],
53+
loggingSinkResourceTypes)
54+
}
55+
return &LoggingSinkId{
56+
resourceType: parts[1],
57+
resourceId: parts[2],
58+
name: parts[3],
59+
}, nil
60+
}

google/logging_utils_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package google
2+
3+
import "testing"
4+
5+
func TestParseLoggingSinkId(t *testing.T) {
6+
tests := []struct {
7+
val string
8+
out *LoggingSinkId
9+
errExpected bool
10+
}{
11+
{"projects/my-project/sinks/my-sink", &LoggingSinkId{"projects", "my-project", "my-sink"}, false},
12+
{"folders/foofolder/sinks/woo", &LoggingSinkId{"folders", "foofolder", "woo"}, false},
13+
{"kitchens/the-big-one/sinks/second-from-the-left", nil, true},
14+
}
15+
16+
for _, test := range tests {
17+
out, err := parseLoggingSinkId(test.val)
18+
if err != nil {
19+
if !test.errExpected {
20+
t.Errorf("Got error with val %#v: error = %#v", test.val, err)
21+
}
22+
} else {
23+
if *out != *test.out {
24+
t.Errorf("Mismatch on val %#v: expected %#v but got %#v", test.val, test.out, out)
25+
}
26+
}
27+
}
28+
}
29+
30+
func TestLoggingSinkId(t *testing.T) {
31+
tests := []struct {
32+
val LoggingSinkId
33+
canonicalId string
34+
parent string
35+
}{
36+
{
37+
val: LoggingSinkId{"projects", "my-project", "my-sink"},
38+
canonicalId: "projects/my-project/sinks/my-sink",
39+
parent: "projects/my-project",
40+
}, {
41+
val: LoggingSinkId{"folders", "foofolder", "woo"},
42+
canonicalId: "folders/foofolder/sinks/woo",
43+
parent: "folders/foofolder",
44+
},
45+
}
46+
47+
for _, test := range tests {
48+
canonicalId := test.val.canonicalId()
49+
50+
if canonicalId != test.canonicalId {
51+
t.Errorf("canonicalId mismatch on val %#v: expected %#v but got %#v", test.val, test.canonicalId, canonicalId)
52+
}
53+
54+
parent := test.val.parent()
55+
56+
if parent != test.parent {
57+
t.Errorf("parent mismatch on val %#v: expected %#v but got %#v", test.val, test.parent, parent)
58+
}
59+
}
60+
}

google/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func Provider() terraform.ResourceProvider {
107107
"google_dns_managed_zone": resourceDnsManagedZone(),
108108
"google_dns_record_set": resourceDnsRecordSet(),
109109
"google_folder": resourceGoogleFolder(),
110+
"google_logging_project_sink": resourceLoggingProjectSink(),
110111
"google_sourcerepo_repository": resourceSourceRepoRepository(),
111112
"google_spanner_instance": resourceSpannerInstance(),
112113
"google_spanner_database": resourceSpannerDatabase(),
+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/terraform/helper/schema"
7+
"google.golang.org/api/logging/v2"
8+
)
9+
10+
const nonUniqueWriterAccount = "serviceAccount:[email protected]"
11+
12+
func resourceLoggingProjectSink() *schema.Resource {
13+
return &schema.Resource{
14+
Create: resourceLoggingProjectSinkCreate,
15+
Read: resourceLoggingProjectSinkRead,
16+
Delete: resourceLoggingProjectSinkDelete,
17+
Update: resourceLoggingProjectSinkUpdate,
18+
Schema: map[string]*schema.Schema{
19+
"name": {
20+
Type: schema.TypeString,
21+
Required: true,
22+
ForceNew: true,
23+
},
24+
25+
"destination": {
26+
Type: schema.TypeString,
27+
Required: true,
28+
},
29+
30+
"filter": {
31+
Type: schema.TypeString,
32+
Optional: true,
33+
},
34+
35+
"project": {
36+
Type: schema.TypeString,
37+
Optional: true,
38+
ForceNew: true,
39+
},
40+
41+
"unique_writer_identity": {
42+
Type: schema.TypeBool,
43+
Optional: true,
44+
Default: false,
45+
ForceNew: true,
46+
},
47+
48+
"writer_identity": {
49+
Type: schema.TypeString,
50+
Computed: true,
51+
},
52+
},
53+
}
54+
}
55+
56+
func resourceLoggingProjectSinkCreate(d *schema.ResourceData, meta interface{}) error {
57+
config := meta.(*Config)
58+
59+
project, err := getProject(d, config)
60+
if err != nil {
61+
return err
62+
}
63+
64+
name := d.Get("name").(string)
65+
66+
id := LoggingSinkId{
67+
resourceType: "projects",
68+
resourceId: project,
69+
name: name,
70+
}
71+
72+
sink := logging.LogSink{
73+
Name: d.Get("name").(string),
74+
Destination: d.Get("destination").(string),
75+
Filter: d.Get("filter").(string),
76+
}
77+
78+
uniqueWriterIdentity := d.Get("unique_writer_identity").(bool)
79+
80+
_, err = config.clientLogging.Projects.Sinks.Create(id.parent(), &sink).UniqueWriterIdentity(uniqueWriterIdentity).Do()
81+
if err != nil {
82+
return err
83+
}
84+
85+
d.SetId(id.canonicalId())
86+
87+
return resourceLoggingProjectSinkRead(d, meta)
88+
}
89+
90+
func resourceLoggingProjectSinkRead(d *schema.ResourceData, meta interface{}) error {
91+
config := meta.(*Config)
92+
93+
sink, err := config.clientLogging.Projects.Sinks.Get(d.Id()).Do()
94+
if err != nil {
95+
return handleNotFoundError(err, d, fmt.Sprintf("Project Logging Sink %s", d.Get("name").(string)))
96+
}
97+
98+
d.Set("name", sink.Name)
99+
d.Set("destination", sink.Destination)
100+
d.Set("filter", sink.Filter)
101+
d.Set("writer_identity", sink.WriterIdentity)
102+
if sink.WriterIdentity != nonUniqueWriterAccount {
103+
d.Set("unique_writer_identity", true)
104+
} else {
105+
d.Set("unique_writer_identity", false)
106+
}
107+
return nil
108+
}
109+
110+
func resourceLoggingProjectSinkUpdate(d *schema.ResourceData, meta interface{}) error {
111+
config := meta.(*Config)
112+
113+
// Can only update destination/filter right now. Despite the method below using 'Patch', the API requires both
114+
// destination and filter (even if unchanged).
115+
sink := logging.LogSink{
116+
Destination: d.Get("destination").(string),
117+
Filter: d.Get("filter").(string),
118+
}
119+
120+
if d.HasChange("destination") {
121+
sink.ForceSendFields = append(sink.ForceSendFields, "Destination")
122+
}
123+
if d.HasChange("filter") {
124+
sink.ForceSendFields = append(sink.ForceSendFields, "Filter")
125+
}
126+
127+
uniqueWriterIdentity := d.Get("unique_writer_identity").(bool)
128+
129+
_, err := config.clientLogging.Projects.Sinks.Patch(d.Id(), &sink).UniqueWriterIdentity(uniqueWriterIdentity).Do()
130+
if err != nil {
131+
return err
132+
}
133+
134+
return resourceLoggingProjectSinkRead(d, meta)
135+
}
136+
137+
func resourceLoggingProjectSinkDelete(d *schema.ResourceData, meta interface{}) error {
138+
config := meta.(*Config)
139+
140+
_, err := config.clientLogging.Projects.Sinks.Delete(d.Id()).Do()
141+
if err != nil {
142+
return err
143+
}
144+
145+
d.SetId("")
146+
return nil
147+
}

0 commit comments

Comments
 (0)