Skip to content

Commit

Permalink
Add stackdriver project sink support (#432)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
selmanj authored Sep 15, 2017
1 parent b694d5a commit 3d5eccc
Show file tree
Hide file tree
Showing 11 changed files with 15,288 additions and 0 deletions.
9 changes: 9 additions & 0 deletions google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"google.golang.org/api/container/v1"
"google.golang.org/api/dns/v1"
"google.golang.org/api/iam/v1"
cloudlogging "google.golang.org/api/logging/v2"
"google.golang.org/api/pubsub/v1"
"google.golang.org/api/runtimeconfig/v1beta1"
"google.golang.org/api/servicemanagement/v1"
Expand All @@ -46,6 +47,7 @@ type Config struct {
clientComputeBeta *computeBeta.Service
clientContainer *container.Service
clientDns *dns.Service
clientLogging *cloudlogging.Service
clientPubsub *pubsub.Service
clientResourceManager *cloudresourcemanager.Service
clientResourceManagerV2Beta1 *resourceManagerV2Beta1.Service
Expand Down Expand Up @@ -153,6 +155,13 @@ func (c *Config) loadAndValidate() error {
}
c.clientDns.UserAgent = userAgent

log.Printf("[INFO] Instantiating Google Stackdriver Logging client...")
c.clientLogging, err = cloudlogging.New(client)
if err != nil {
return err
}
c.clientLogging.UserAgent = userAgent

log.Printf("[INFO] Instantiating Google Storage Client...")
c.clientStorage, err = storage.New(client)
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions google/logging_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package google

import (
"fmt"
"regexp"
)

// loggingSinkResourceTypes contains all the possible Stackdriver Logging resource types. Used to parse ids safely.
var loggingSinkResourceTypes = []string{
"billingAccount",
"folders",
"organizations",
"projects",
}

// LoggingSinkId represents the parts that make up the canonical id used within terraform for a logging resource.
type LoggingSinkId struct {
resourceType string
resourceId string
name string
}

// loggingSinkIdRegex matches valid logging sink canonical ids
var loggingSinkIdRegex = regexp.MustCompile("(.+)/(.+)/sinks/(.+)")

// canonicalId returns the LoggingSinkId as the canonical id used within terraform.
func (l LoggingSinkId) canonicalId() string {
return fmt.Sprintf("%s/%s/sinks/%s", l.resourceType, l.resourceId, l.name)
}

// parent returns the "parent-level" resource that the sink is in (e.g. `folders/foo` for id `folders/foo/sinks/bar`)
func (l LoggingSinkId) parent() string {
return fmt.Sprintf("%s/%s", l.resourceType, l.resourceId)
}

// parseLoggingSinkId parses a canonical id into a LoggingSinkId, or returns an error on failure.
func parseLoggingSinkId(id string) (*LoggingSinkId, error) {
parts := loggingSinkIdRegex.FindStringSubmatch(id)
if parts == nil {
return nil, fmt.Errorf("unable to parse logging sink id %#v", id)
}
// If our resourceType is not a valid logging sink resource type, complain loudly
validLoggingSinkResourceType := false
for _, v := range loggingSinkResourceTypes {
if v == parts[1] {
validLoggingSinkResourceType = true
break
}
}

if !validLoggingSinkResourceType {
return nil, fmt.Errorf("Logging resource type %s is not valid. Valid resource types: %#v", parts[1],
loggingSinkResourceTypes)
}
return &LoggingSinkId{
resourceType: parts[1],
resourceId: parts[2],
name: parts[3],
}, nil
}
60 changes: 60 additions & 0 deletions google/logging_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package google

import "testing"

func TestParseLoggingSinkId(t *testing.T) {
tests := []struct {
val string
out *LoggingSinkId
errExpected bool
}{
{"projects/my-project/sinks/my-sink", &LoggingSinkId{"projects", "my-project", "my-sink"}, false},
{"folders/foofolder/sinks/woo", &LoggingSinkId{"folders", "foofolder", "woo"}, false},
{"kitchens/the-big-one/sinks/second-from-the-left", nil, true},
}

for _, test := range tests {
out, err := parseLoggingSinkId(test.val)
if err != nil {
if !test.errExpected {
t.Errorf("Got error with val %#v: error = %#v", test.val, err)
}
} else {
if *out != *test.out {
t.Errorf("Mismatch on val %#v: expected %#v but got %#v", test.val, test.out, out)
}
}
}
}

func TestLoggingSinkId(t *testing.T) {
tests := []struct {
val LoggingSinkId
canonicalId string
parent string
}{
{
val: LoggingSinkId{"projects", "my-project", "my-sink"},
canonicalId: "projects/my-project/sinks/my-sink",
parent: "projects/my-project",
}, {
val: LoggingSinkId{"folders", "foofolder", "woo"},
canonicalId: "folders/foofolder/sinks/woo",
parent: "folders/foofolder",
},
}

for _, test := range tests {
canonicalId := test.val.canonicalId()

if canonicalId != test.canonicalId {
t.Errorf("canonicalId mismatch on val %#v: expected %#v but got %#v", test.val, test.canonicalId, canonicalId)
}

parent := test.val.parent()

if parent != test.parent {
t.Errorf("parent mismatch on val %#v: expected %#v but got %#v", test.val, test.parent, parent)
}
}
}
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func Provider() terraform.ResourceProvider {
"google_dns_managed_zone": resourceDnsManagedZone(),
"google_dns_record_set": resourceDnsRecordSet(),
"google_folder": resourceGoogleFolder(),
"google_logging_project_sink": resourceLoggingProjectSink(),
"google_sourcerepo_repository": resourceSourceRepoRepository(),
"google_spanner_instance": resourceSpannerInstance(),
"google_spanner_database": resourceSpannerDatabase(),
Expand Down
147 changes: 147 additions & 0 deletions google/resource_logging_project_sink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package google

import (
"fmt"

"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/logging/v2"
)

const nonUniqueWriterAccount = "serviceAccount:[email protected]"

func resourceLoggingProjectSink() *schema.Resource {
return &schema.Resource{
Create: resourceLoggingProjectSinkCreate,
Read: resourceLoggingProjectSinkRead,
Delete: resourceLoggingProjectSinkDelete,
Update: resourceLoggingProjectSinkUpdate,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"destination": {
Type: schema.TypeString,
Required: true,
},

"filter": {
Type: schema.TypeString,
Optional: true,
},

"project": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},

"unique_writer_identity": {
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},

"writer_identity": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceLoggingProjectSinkCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

project, err := getProject(d, config)
if err != nil {
return err
}

name := d.Get("name").(string)

id := LoggingSinkId{
resourceType: "projects",
resourceId: project,
name: name,
}

sink := logging.LogSink{
Name: d.Get("name").(string),
Destination: d.Get("destination").(string),
Filter: d.Get("filter").(string),
}

uniqueWriterIdentity := d.Get("unique_writer_identity").(bool)

_, err = config.clientLogging.Projects.Sinks.Create(id.parent(), &sink).UniqueWriterIdentity(uniqueWriterIdentity).Do()
if err != nil {
return err
}

d.SetId(id.canonicalId())

return resourceLoggingProjectSinkRead(d, meta)
}

func resourceLoggingProjectSinkRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

sink, err := config.clientLogging.Projects.Sinks.Get(d.Id()).Do()
if err != nil {
return handleNotFoundError(err, d, fmt.Sprintf("Project Logging Sink %s", d.Get("name").(string)))
}

d.Set("name", sink.Name)
d.Set("destination", sink.Destination)
d.Set("filter", sink.Filter)
d.Set("writer_identity", sink.WriterIdentity)
if sink.WriterIdentity != nonUniqueWriterAccount {
d.Set("unique_writer_identity", true)
} else {
d.Set("unique_writer_identity", false)
}
return nil
}

func resourceLoggingProjectSinkUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

// Can only update destination/filter right now. Despite the method below using 'Patch', the API requires both
// destination and filter (even if unchanged).
sink := logging.LogSink{
Destination: d.Get("destination").(string),
Filter: d.Get("filter").(string),
}

if d.HasChange("destination") {
sink.ForceSendFields = append(sink.ForceSendFields, "Destination")
}
if d.HasChange("filter") {
sink.ForceSendFields = append(sink.ForceSendFields, "Filter")
}

uniqueWriterIdentity := d.Get("unique_writer_identity").(bool)

_, err := config.clientLogging.Projects.Sinks.Patch(d.Id(), &sink).UniqueWriterIdentity(uniqueWriterIdentity).Do()
if err != nil {
return err
}

return resourceLoggingProjectSinkRead(d, meta)
}

func resourceLoggingProjectSinkDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

_, err := config.clientLogging.Projects.Sinks.Delete(d.Id()).Do()
if err != nil {
return err
}

d.SetId("")
return nil
}
Loading

0 comments on commit 3d5eccc

Please sign in to comment.