diff --git a/deploy/load_balancer.go b/deploy/load_balancer.go index cb1e68f..5f8dd57 100644 --- a/deploy/load_balancer.go +++ b/deploy/load_balancer.go @@ -167,11 +167,11 @@ func (o *LoadBalancer) processDomain(pctx *config.PluginContext, r *registry.Reg } if domainInfo != nil { - if domainInfo.Other.GetFields() == nil { - domainInfo.Other, _ = structpb.NewStruct(nil) + if domainInfo.Properties.GetFields() == nil { + domainInfo.Properties, _ = structpb.NewStruct(nil) } - domainInfo.Other.GetFields()["cloudflare_proxy"] = structpb.NewBoolValue(false) + domainInfo.Properties.GetFields()["cloudflare_proxy"] = structpb.NewBoolValue(false) } o.ManagedSSLs = append(o.ManagedSSLs, cert) diff --git a/gcp/bucket.go b/gcp/bucket.go index 21397da..e95c141 100644 --- a/gcp/bucket.go +++ b/gcp/bucket.go @@ -105,7 +105,7 @@ func (o *Bucket) Read(ctx context.Context, meta interface{}) error { return fmt.Errorf("error fetching bucket policy: %w", err) } - o.Public.SetCurrent(policy.HasRole("allUsers", "roles/storage.objectViewer")) + o.Public.SetCurrent(policy.HasRole(ACLAllUsers, "roles/storage.objectViewer")) return nil } @@ -173,7 +173,7 @@ func (o *Bucket) Create(ctx context.Context, meta interface{}) error { return fmt.Errorf("error fetching bucket policy: %w", err) } - policy.Add("allUsers", "roles/storage.objectViewer") + policy.Add(ACLAllUsers, "roles/storage.objectViewer") err = b.IAM().SetPolicy(ctx, policy) if err != nil { @@ -252,9 +252,9 @@ func (o *Bucket) Update(ctx context.Context, meta interface{}) error { } if o.Public.Wanted() { - policy.Add("allUsers", "roles/storage.objectViewer") + policy.Add(ACLAllUsers, "roles/storage.objectViewer") } else { - policy.Remove("allUsers", "roles/storage.objectViewer") + policy.Remove(ACLAllUsers, "roles/storage.objectViewer") } err = b.IAM().SetPolicy(ctx, policy) diff --git a/gcp/cloud_function.go b/gcp/cloud_function.go index 86b7e14..14a9080 100644 --- a/gcp/cloud_function.go +++ b/gcp/cloud_function.go @@ -114,7 +114,7 @@ func (o *CloudFunction) Read(ctx context.Context, meta interface{}) error { for _, b := range policy.Bindings { if b.Role == "roles/cloudfunctions.invoker" { - isPublic = plugin_util.StringSliceContains(b.Members, "allUsers") + isPublic = plugin_util.StringSliceContains(b.Members, ACLAllUsers) break } } @@ -194,7 +194,7 @@ func (o *CloudFunction) Create(ctx context.Context, meta interface{}) error { } policy.Bindings = append(policy.Bindings, &cloudfunctions.Binding{ - Members: []string{"allUsers"}, + Members: []string{ACLAllUsers}, Role: "roles/cloudfunctions.invoker", }) @@ -251,7 +251,7 @@ func (o *CloudFunction) Update(ctx context.Context, meta interface{}) error { for _, b := range policy.Bindings { if b.Role == "roles/cloudfunctions.invoker" { if !o.IsPublic.Wanted() { - if !plugin_util.StringSliceContains(b.Members, "allUsers") { + if !plugin_util.StringSliceContains(b.Members, ACLAllUsers) { newBindings = append(newBindings, b) continue } @@ -264,14 +264,14 @@ func (o *CloudFunction) Update(ctx context.Context, meta interface{}) error { var newMembers []string for _, m := range b.Members { - if m != "allUsers" { + if m != ACLAllUsers { newMembers = append(newMembers, m) } } b.Members = newMembers - } else if !plugin_util.StringSliceContains(b.Members, "allUsers") { - b.Members = append(b.Members, "allUsers") + } else if !plugin_util.StringSliceContains(b.Members, ACLAllUsers) { + b.Members = append(b.Members, ACLAllUsers) added = true } } @@ -281,7 +281,7 @@ func (o *CloudFunction) Update(ctx context.Context, meta interface{}) error { if o.IsPublic.Wanted() && !added { newBindings = append(newBindings, &cloudfunctions.Binding{ - Members: []string{"allUsers"}, + Members: []string{ACLAllUsers}, Role: "roles/cloudfunctions.invoker", }) } diff --git a/gcp/cloud_run.go b/gcp/cloud_run.go index 3ff8ab0..e64c419 100644 --- a/gcp/cloud_run.go +++ b/gcp/cloud_run.go @@ -148,7 +148,7 @@ func (o *CloudRun) Read(ctx context.Context, meta interface{}) error { // nolint return err } - if err == nil && pol != nil && len(pol.Bindings) == 1 && len(pol.Bindings[0].Members) == 1 && pol.Bindings[0].Role == "roles/run.invoker" && pol.Bindings[0].Members[0] == "allUsers" { + if err == nil && pol != nil && len(pol.Bindings) == 1 && len(pol.Bindings[0].Members) == 1 && pol.Bindings[0].Role == "roles/run.invoker" && pol.Bindings[0].Members[0] == ACLAllUsers { o.IsPublic.SetCurrent(true) } else { o.IsPublic.SetCurrent(false) @@ -334,7 +334,7 @@ func setRunServiceIAMPolicy(cli *run.APIService, project, region, name string, p if public { policy = &run.Policy{Bindings: []*run.Binding{{ - Members: []string{"allUsers"}, + Members: []string{ACLAllUsers}, Role: "roles/run.invoker", }}} } diff --git a/gcp/consts.go b/gcp/consts.go index 1c5347c..0a88849 100644 --- a/gcp/consts.go +++ b/gcp/consts.go @@ -1,13 +1,14 @@ package gcp var ( - APISRequired = []string{"run.googleapis.com", "containerregistry.googleapis.com", "compute.googleapis.com", "sqladmin.googleapis.com", "secretmanager.googleapis.com", "cloudresourcemanager.googleapis.com", "cloudfunctions.googleapis.com"} + APISRequired = []string{"run.googleapis.com", "containerregistry.googleapis.com", "compute.googleapis.com", "sqladmin.googleapis.com", "secretmanager.googleapis.com", "cloudresourcemanager.googleapis.com", "cloudfunctions.googleapis.com", "monitoring.googleapis.com"} ValidRegions = []string{"asia-east1", "asia-east2", "asia-northeast1", "asia-northeast2", "asia-northeast3", "asia-south1", "asia-southeast1", "australia-southeast1", "europe-north1", "europe-west1", "europe-west2", "europe-west3", "europe-west4", "europe-west6", "northamerica-northeast1", "southamerica-east1", "us-central1", "us-east1", "us-east4", "us-west1", "us-west2", "us-west3"} ) const ( ACLPublicRead = "publicRead" ACLProjectPrivate = "projectPrivate" + ACLAllUsers = "allUsers" OperationDone = "DONE" CloudRunReady = "Ready" diff --git a/gcp/notification_channel.go b/gcp/notification_channel.go new file mode 100644 index 0000000..2dbf22a --- /dev/null +++ b/gcp/notification_channel.go @@ -0,0 +1,148 @@ +package gcp + +import ( + "context" + "fmt" + + "github.com/outblocks/cli-plugin-gcp/internal/config" + "github.com/outblocks/outblocks-plugin-go/registry" + "github.com/outblocks/outblocks-plugin-go/registry/fields" + "google.golang.org/genproto/googleapis/monitoring/v3" +) + +type NotificationChannel struct { + registry.ResourceBase + + ID fields.StringOutputField + DisplayName fields.StringInputField `default:"Outblocks Notification Channel"` + ProjectID fields.StringInputField `state:"force_new"` + Type fields.StringInputField + Labels fields.MapInputField +} + +func (o *NotificationChannel) ReferenceID() string { + return o.ID.Current() +} + +func (o *NotificationChannel) GetName() string { + return fields.VerboseString(o.DisplayName) +} + +func (o *NotificationChannel) Read(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringNotificationChannelClient(ctx) + if err != nil { + return err + } + + projectID := o.ProjectID.Any() + id := o.ID.Current() + + if id == "" { + return nil + } + + obj, err := cli.GetNotificationChannel(ctx, &monitoring.GetNotificationChannelRequest{ + Name: id, + }) + if ErrIs404(err) { + o.MarkAsNew() + + return nil + } else if err != nil { + return err + } + + o.MarkAsExisting() + o.ProjectID.SetCurrent(projectID) + o.DisplayName.SetCurrent(obj.DisplayName) + o.Type.SetCurrent(obj.Type) + + labels := make(map[string]interface{}, len(obj.Labels)) + + for k, v := range obj.Labels { + labels[k] = v + } + + o.Labels.SetCurrent(labels) + + return nil +} + +func (o *NotificationChannel) createNotificationChannel(update bool) *monitoring.NotificationChannel { + displayName := o.DisplayName.Wanted() + typ := o.Type.Wanted() + + labels := o.Labels.Wanted() + labelsMap := make(map[string]string, len(labels)) + + for k, v := range labels { + labelsMap[k] = v.(string) + } + + cfg := &monitoring.NotificationChannel{ + DisplayName: displayName, + Type: typ, + Labels: labelsMap, + } + + if update { + cfg.Name = o.ID.Current() + } + + return cfg +} + +func (o *NotificationChannel) Create(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringNotificationChannelClient(ctx) + if err != nil { + return err + } + + projectID := o.ProjectID.Wanted() + + obj, err := cli.CreateNotificationChannel(ctx, &monitoring.CreateNotificationChannelRequest{ + Name: fmt.Sprintf("projects/%s", projectID), + NotificationChannel: o.createNotificationChannel(false), + }) + if err != nil { + return err + } + + o.ID.SetCurrent(obj.Name) + + return err +} + +func (o *NotificationChannel) Update(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringNotificationChannelClient(ctx) + if err != nil { + return err + } + + _, err = cli.UpdateNotificationChannel(ctx, &monitoring.UpdateNotificationChannelRequest{ + NotificationChannel: o.createNotificationChannel(true), + }) + + return err +} + +func (o *NotificationChannel) Delete(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringNotificationChannelClient(ctx) + if err != nil { + return err + } + + err = cli.DeleteNotificationChannel(ctx, &monitoring.DeleteNotificationChannelRequest{ + Name: o.ID.Current(), + }) + + return err +} diff --git a/gcp/uptime_alert_policy.go b/gcp/uptime_alert_policy.go new file mode 100644 index 0000000..50d9978 --- /dev/null +++ b/gcp/uptime_alert_policy.go @@ -0,0 +1,183 @@ +package gcp + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/outblocks/cli-plugin-gcp/internal/config" + "github.com/outblocks/outblocks-plugin-go/registry" + "github.com/outblocks/outblocks-plugin-go/registry/fields" + "google.golang.org/genproto/googleapis/monitoring/v3" + "google.golang.org/protobuf/types/known/durationpb" +) + +type UptimeAlertPolicy struct { + registry.ResourceBase + + ID fields.StringOutputField + DisplayName fields.StringInputField `default:"Outblocks Notification Channel"` + ProjectID fields.StringInputField `state:"force_new"` + CheckID fields.StringInputField + NotificationChannelIDs fields.ArrayInputField +} + +func (o *UptimeAlertPolicy) ReferenceID() string { + return o.ID.Current() +} + +func (o *UptimeAlertPolicy) GetName() string { + return fields.VerboseString(o.DisplayName) +} + +func (o *UptimeAlertPolicy) Read(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringAlertPolicyClient(ctx) + if err != nil { + return err + } + + projectID := o.ProjectID.Any() + id := o.ID.Current() + + if id == "" { + return nil + } + + obj, err := cli.GetAlertPolicy(ctx, &monitoring.GetAlertPolicyRequest{ + Name: id, + }) + if ErrIs404(err) { + o.MarkAsNew() + + return nil + } else if err != nil { + return err + } + + o.MarkAsExisting() + o.ProjectID.SetCurrent(projectID) + o.DisplayName.SetCurrent(obj.DisplayName) + + channels := make([]interface{}, len(obj.NotificationChannels)) + for i, v := range obj.NotificationChannels { + channels[i] = v + } + + o.NotificationChannelIDs.SetCurrent(channels) + + o.CheckID.UnsetCurrent() + + if len(obj.Conditions) == 1 { + s := strings.Split(obj.Conditions[0].GetConditionThreshold().Filter, "metric.labels.check_id = \"") + if len(s) != 2 { + return nil + } + + s = strings.Split(s[1], "\"") + + o.CheckID.SetCurrent(s[0]) + } + + return nil +} + +func (o *UptimeAlertPolicy) createAlertPolicy(update bool) *monitoring.AlertPolicy { + displayName := o.DisplayName.Wanted() + checkID := o.CheckID.Wanted() + channels := o.NotificationChannelIDs.Wanted() + channelsStr := make([]string, len(channels)) + + for i, v := range channels { + channelsStr[i] = v.(string) + } + + cfg := &monitoring.AlertPolicy{ + DisplayName: displayName, + Conditions: []*monitoring.AlertPolicy_Condition{ + { + DisplayName: "uptime check", + Condition: &monitoring.AlertPolicy_Condition_ConditionThreshold{ + ConditionThreshold: &monitoring.AlertPolicy_Condition_MetricThreshold{ + Filter: fmt.Sprintf("resource.type = \"uptime_url\" AND metric.type = \"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id = \"%s\"", checkID), + Duration: durationpb.New(60 * time.Second), + Comparison: monitoring.ComparisonType_COMPARISON_GT, + ThresholdValue: 2, + + Aggregations: []*monitoring.Aggregation{ + { + AlignmentPeriod: durationpb.New(1200 * time.Second), + CrossSeriesReducer: monitoring.Aggregation_REDUCE_COUNT_FALSE, + PerSeriesAligner: monitoring.Aggregation_ALIGN_NEXT_OLDER, + GroupByFields: []string{"resource.*"}, + }, + }, + }, + }, + }, + }, + Combiner: monitoring.AlertPolicy_OR, + NotificationChannels: channelsStr, + } + + if update { + cfg.Name = o.ID.Current() + } + + return cfg +} + +func (o *UptimeAlertPolicy) Create(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringAlertPolicyClient(ctx) + if err != nil { + return err + } + + projectID := o.ProjectID.Wanted() + + obj, err := cli.CreateAlertPolicy(ctx, &monitoring.CreateAlertPolicyRequest{ + Name: fmt.Sprintf("projects/%s", projectID), + AlertPolicy: o.createAlertPolicy(false), + }) + if err != nil { + return err + } + + o.ID.SetCurrent(obj.Name) + + return err +} + +func (o *UptimeAlertPolicy) Update(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringAlertPolicyClient(ctx) + if err != nil { + return err + } + + _, err = cli.UpdateAlertPolicy(ctx, &monitoring.UpdateAlertPolicyRequest{ + AlertPolicy: o.createAlertPolicy(true), + }) + + return err +} + +func (o *UptimeAlertPolicy) Delete(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringAlertPolicyClient(ctx) + if err != nil { + return err + } + + err = cli.DeleteAlertPolicy(ctx, &monitoring.DeleteAlertPolicyRequest{ + Name: o.ID.Current(), + }) + + return err +} diff --git a/gcp/uptime_check_config.go b/gcp/uptime_check_config.go new file mode 100644 index 0000000..dd4e9d4 --- /dev/null +++ b/gcp/uptime_check_config.go @@ -0,0 +1,229 @@ +package gcp + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/outblocks/cli-plugin-gcp/internal/config" + "github.com/outblocks/outblocks-plugin-go/registry" + "github.com/outblocks/outblocks-plugin-go/registry/fields" + "google.golang.org/genproto/googleapis/api/monitoredres" + "google.golang.org/genproto/googleapis/monitoring/v3" + "google.golang.org/protobuf/types/known/durationpb" +) + +type UptimeCheckConfig struct { + registry.ResourceBase + + ID fields.StringOutputField + DisplayName fields.StringInputField `default:"Outblocks Uptime Check"` + ProjectID fields.StringInputField `state:"force_new"` + URL fields.StringInputField + Frequency fields.IntInputField `default:"5"` + Timeout fields.IntInputField `default:"60"` + Regions fields.ArrayInputField +} + +func (o *UptimeCheckConfig) ReferenceID() string { + return o.ID.Current() +} + +func (o *UptimeCheckConfig) GetName() string { + return fields.VerboseString(o.DisplayName) +} + +func (o *UptimeCheckConfig) Read(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringUptimeCheckClient(ctx) + if err != nil { + return err + } + + projectID := o.ProjectID.Any() + id := o.ID.Current() + + if id == "" { + return nil + } + + obj, err := cli.GetUptimeCheckConfig(ctx, &monitoring.GetUptimeCheckConfigRequest{ + Name: id, + }) + if ErrIs404(err) { + o.MarkAsNew() + + return nil + } else if err != nil { + return err + } + + o.MarkAsExisting() + o.ProjectID.SetCurrent(projectID) + o.DisplayName.SetCurrent(obj.DisplayName) + + o.URL.UnsetCurrent() + + if obj.GetMonitoredResource().GetType() == "uptime_url" { + u := "http://" + + if obj.GetHttpCheck().GetUseSsl() { + u = "https://" + } + + u = fmt.Sprintf("%s%s%s", u, obj.GetMonitoredResource().GetLabels()["host"], obj.GetHttpCheck().GetPath()) + o.URL.SetCurrent(u) + } + + o.Frequency.SetCurrent(int(obj.Period.AsDuration() / time.Minute)) + o.Timeout.SetCurrent(int(obj.Timeout.AsDuration() / time.Second)) + + wantedRegions := make(map[monitoring.UptimeCheckRegion]struct{}) + + for _, reg := range o.Regions.Wanted() { + r := stringToUptimeCheckRegion(reg.(string)) + + if r == monitoring.UptimeCheckRegion_REGION_UNSPECIFIED { + continue + } + + wantedRegions[r] = struct{}{} + } + + for _, reg := range obj.SelectedRegions { + delete(wantedRegions, reg) + } + + if len(wantedRegions) == 0 { + o.Regions.SetCurrent(o.Regions.Wanted()) + } else { + regs := make([]interface{}, len(obj.SelectedRegions)) + for i, r := range obj.SelectedRegions { + regs[i] = r.String() + } + + o.Regions.SetCurrent(regs) + } + + return nil +} + +func stringToUptimeCheckRegion(r string) monitoring.UptimeCheckRegion { + switch strings.ToLower(r) { + case "usa": + return monitoring.UptimeCheckRegion_USA + case "europe": + return monitoring.UptimeCheckRegion_EUROPE + case "south_america": + return monitoring.UptimeCheckRegion_SOUTH_AMERICA + case "asia": + return monitoring.UptimeCheckRegion_ASIA_PACIFIC + } + + return monitoring.UptimeCheckRegion_REGION_UNSPECIFIED +} + +func (o *UptimeCheckConfig) createUptimeCheckConfig(update bool) *monitoring.UptimeCheckConfig { + projectID := o.ProjectID.Wanted() + displayName := o.DisplayName.Wanted() + u, _ := url.Parse(o.URL.Wanted()) + freq := o.Frequency.Wanted() + timeout := o.Timeout.Wanted() + regions := o.Regions.Wanted() + + var selRegions []monitoring.UptimeCheckRegion + + for _, reg := range regions { + r := stringToUptimeCheckRegion(reg.(string)) + if r == monitoring.UptimeCheckRegion_REGION_UNSPECIFIED { + continue + } + + selRegions = append(selRegions, r) + } + + cfg := &monitoring.UptimeCheckConfig{ + DisplayName: displayName, + Resource: &monitoring.UptimeCheckConfig_MonitoredResource{ + MonitoredResource: &monitoredres.MonitoredResource{ + Type: "uptime_url", + Labels: map[string]string{ + "project_id": projectID, + "host": u.Hostname(), + }, + }, + }, + CheckRequestType: &monitoring.UptimeCheckConfig_HttpCheck_{ + HttpCheck: &monitoring.UptimeCheckConfig_HttpCheck{ + RequestMethod: monitoring.UptimeCheckConfig_HttpCheck_GET, + UseSsl: u.Scheme == "https", + Path: u.Path, + }, + }, + Period: durationpb.New(time.Duration(freq) * time.Minute), + Timeout: durationpb.New(time.Duration(timeout) * time.Second), + SelectedRegions: selRegions, + } + + if update { + cfg.Name = o.ID.Current() + } + + return cfg +} + +func (o *UptimeCheckConfig) Create(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringUptimeCheckClient(ctx) + if err != nil { + return err + } + + projectID := o.ProjectID.Wanted() + + obj, err := cli.CreateUptimeCheckConfig(ctx, &monitoring.CreateUptimeCheckConfigRequest{ + Parent: fmt.Sprintf("projects/%s", projectID), + UptimeCheckConfig: o.createUptimeCheckConfig(false), + }) + if err != nil { + return err + } + + o.ID.SetCurrent(obj.Name) + + return err +} + +func (o *UptimeCheckConfig) Update(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringUptimeCheckClient(ctx) + if err != nil { + return err + } + + _, err = cli.UpdateUptimeCheckConfig(ctx, &monitoring.UpdateUptimeCheckConfigRequest{ + UptimeCheckConfig: o.createUptimeCheckConfig(true), + }) + + return err +} + +func (o *UptimeCheckConfig) Delete(ctx context.Context, meta interface{}) error { + pctx := meta.(*config.PluginContext) + + cli, err := pctx.GCPMonitoringUptimeCheckClient(ctx) + if err != nil { + return err + } + + err = cli.DeleteUptimeCheckConfig(ctx, &monitoring.DeleteUptimeCheckConfigRequest{ + Name: o.ID.Current(), + }) + + return err +} diff --git a/gcp/util.go b/gcp/util.go index 54493ab..54af0c3 100644 --- a/gcp/util.go +++ b/gcp/util.go @@ -41,6 +41,9 @@ var Types = []registry.Resource{ (*TargetHTTPProxy)(nil), (*TargetHTTPSProxy)(nil), (*URLMap)(nil), + (*UptimeCheckConfig)(nil), + (*UptimeAlertPolicy)(nil), + (*NotificationChannel)(nil), } func RegisterTypes(reg *registry.Registry) { diff --git a/go.mod b/go.mod index 6076418..0015f27 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,13 @@ go 1.18 require ( cloud.google.com/go/logging v1.4.2 + cloud.google.com/go/monitoring v1.5.0 cloud.google.com/go/storage v1.22.1 github.com/creasty/defaults v1.6.0 github.com/docker/docker v20.10.17+incompatible github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/google/go-containerregistry v0.9.0 - github.com/outblocks/outblocks-plugin-go v0.0.0-20220712161354-7d8111dfc469 + github.com/outblocks/outblocks-plugin-go v0.0.0-20220803192450-7744c1c50028 golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 google.golang.org/api v0.85.0 google.golang.org/genproto v0.0.0-20220622171453-ea41d75dfa0f diff --git a/go.sum b/go.sum index 1ff7501..78d494f 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/logging v1.4.2 h1:Mu2Q75VBDQlW1HlBMjTX4X84UFR73G1TiLlRYc/b7tA= cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo= +cloud.google.com/go/monitoring v1.5.0 h1:ZltYv8e69fJVga7RTthUBGdx4+Pwz6GRF1V3zylERl4= +cloud.google.com/go/monitoring v1.5.0/go.mod h1:/o9y8NYX5j91JjD/JvGLYbi86kL11OjyJXq2XziLJu4= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -560,8 +562,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/outblocks/outblocks-plugin-go v0.0.0-20220712161354-7d8111dfc469 h1:KviouI7jaoEij8qlvLlKf6EMcUwCWWrwtQ7kqFo+OnY= -github.com/outblocks/outblocks-plugin-go v0.0.0-20220712161354-7d8111dfc469/go.mod h1:Wnb50otE4YCKHxyxpnJi1G3iYVW5pnTAeLHpo3Mu0Lk= +github.com/outblocks/outblocks-plugin-go v0.0.0-20220803192450-7744c1c50028 h1:PjHUEGVTudKUesESpBm9ukAIwvMYvTZTsOpBOQqITGI= +github.com/outblocks/outblocks-plugin-go v0.0.0-20220803192450-7744c1c50028/go.mod h1:Wnb50otE4YCKHxyxpnJi1G3iYVW5pnTAeLHpo3Mu0Lk= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/internal/config/config.go b/internal/config/config.go index 422b268..18d5d2f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "os" logging "cloud.google.com/go/logging/apiv2" + monitoring "cloud.google.com/go/monitoring/apiv3/v2" "cloud.google.com/go/storage" dockerclient "github.com/docker/docker/client" "golang.org/x/oauth2/google" @@ -88,3 +89,15 @@ func NewGCPSecretManagerClient(ctx context.Context, cred *google.Credentials) (* func NewGCPCloudfunctionsClient(ctx context.Context, cred *google.Credentials) (*cloudfunctions.Service, error) { return cloudfunctions.NewService(ctx, option.WithCredentials(cred)) } + +func NewGCPMonitoringUptimeCheckClient(ctx context.Context, cred *google.Credentials) (*monitoring.UptimeCheckClient, error) { + return monitoring.NewUptimeCheckClient(ctx, option.WithCredentials(cred)) +} + +func NewGCPMonitoringNotificationChannelClient(ctx context.Context, cred *google.Credentials) (*monitoring.NotificationChannelClient, error) { + return monitoring.NewNotificationChannelClient(ctx, option.WithCredentials(cred)) +} + +func NewGCPMonitoringAlertPolicyClient(ctx context.Context, cred *google.Credentials) (*monitoring.AlertPolicyClient, error) { + return monitoring.NewAlertPolicyClient(ctx, option.WithCredentials(cred)) +} diff --git a/internal/config/pluginctx.go b/internal/config/pluginctx.go index 4adc5ff..a9a3d15 100644 --- a/internal/config/pluginctx.go +++ b/internal/config/pluginctx.go @@ -5,6 +5,7 @@ import ( "fmt" "sync" + monitoring "cloud.google.com/go/monitoring/apiv3/v2" "cloud.google.com/go/storage" dockerclient "github.com/docker/docker/client" "github.com/outblocks/outblocks-plugin-go/env" @@ -26,13 +27,16 @@ type PluginContext struct { gcred *google.Credentials settings *Settings - storageCli *storage.Client - dockerCli *dockerclient.Client - runCliMap map[string]*run.APIService - computeCli *compute.Service - serviceusageCli *serviceusage.Service - sqlAdminCli *sqladmin.Service - cloudfunctionsCli *cloudfunctions.Service + storageCli *storage.Client + dockerCli *dockerclient.Client + runCliMap map[string]*run.APIService + computeCli *compute.Service + serviceusageCli *serviceusage.Service + sqlAdminCli *sqladmin.Service + cloudfunctionsCli *cloudfunctions.Service + monitoringUptimeChecksCli *monitoring.UptimeCheckClient + monitoringNotificationChannelCli *monitoring.NotificationChannelClient + monitoringAlertPolicyCli *monitoring.AlertPolicyClient funcCache map[string]*funcCacheData @@ -40,7 +44,8 @@ type PluginContext struct { runCli, funcCache sync.Mutex } once struct { - storageCli, dockerCli, computeCli, serviceusageCli, sqlAdminCli, cloudfunctionsCli sync.Once + storageCli, dockerCli, computeCli, serviceusageCli, sqlAdminCli, cloudfunctionsCli, + monitoringUptimeChecksCli, monitoringNotificationChannelCli, monitoringAlertPolicyCli sync.Once } } @@ -150,12 +155,54 @@ func (c *PluginContext) GCPCloudfunctionsClient(ctx context.Context) (*cloudfunc }) if err != nil { - return nil, fmt.Errorf("error creating gcp sqladmin client: %w", err) + return nil, fmt.Errorf("error creating gcp cloud functions client: %w", err) } return c.cloudfunctionsCli, err } +func (c *PluginContext) GCPMonitoringAlertPolicyClient(ctx context.Context) (*monitoring.AlertPolicyClient, error) { + var err error + + c.once.monitoringAlertPolicyCli.Do(func() { + c.monitoringAlertPolicyCli, err = NewGCPMonitoringAlertPolicyClient(ctx, c.GoogleCredentials()) + }) + + if err != nil { + return nil, fmt.Errorf("error creating gcp monitoring alert policy client: %w", err) + } + + return c.monitoringAlertPolicyCli, err +} + +func (c *PluginContext) GCPMonitoringUptimeCheckClient(ctx context.Context) (*monitoring.UptimeCheckClient, error) { + var err error + + c.once.monitoringUptimeChecksCli.Do(func() { + c.monitoringUptimeChecksCli, err = NewGCPMonitoringUptimeCheckClient(ctx, c.GoogleCredentials()) + }) + + if err != nil { + return nil, fmt.Errorf("error creating gcp monitoring uptime check client: %w", err) + } + + return c.monitoringUptimeChecksCli, err +} + +func (c *PluginContext) GCPMonitoringNotificationChannelClient(ctx context.Context) (*monitoring.NotificationChannelClient, error) { + var err error + + c.once.monitoringNotificationChannelCli.Do(func() { + c.monitoringNotificationChannelCli, err = NewGCPMonitoringNotificationChannelClient(ctx, c.GoogleCredentials()) + }) + + if err != nil { + return nil, fmt.Errorf("error creating gcp monitoring notification channel client: %w", err) + } + + return c.monitoringNotificationChannelCli, err +} + func (c *PluginContext) DockerClient() (*dockerclient.Client, error) { var err error diff --git a/plugin.yaml b/plugin.yaml index 7b98a84..9de7e59 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -10,6 +10,7 @@ actions: - lock - state - secrets + - monitoring commands: dbproxy: short: Create a database proxy diff --git a/plugin/apply.go b/plugin/deploy.go similarity index 61% rename from plugin/apply.go rename to plugin/deploy.go index e5b587b..2ba07a0 100644 --- a/plugin/apply.go +++ b/plugin/deploy.go @@ -1,12 +1,35 @@ package plugin import ( + "context" + "github.com/outblocks/cli-plugin-gcp/actions" plugin_go "github.com/outblocks/outblocks-plugin-go" apiv1 "github.com/outblocks/outblocks-plugin-go/gen/api/v1" "github.com/outblocks/outblocks-plugin-go/registry" ) +func (p *Plugin) Plan(ctx context.Context, reg *registry.Registry, r *apiv1.PlanRequest) (*apiv1.PlanResponse, error) { + a, err := actions.NewPlan(p.PluginContext(), p.log, r.State, r.Domains, reg, r.Destroy, false) + if err != nil { + return nil, err + } + + deployPlan, err := a.Plan(ctx, r.Apps, r.Dependencies) + if err != nil { + return nil, err + } + + return &apiv1.PlanResponse{ + Plan: deployPlan, + + State: a.State, + AppStates: a.AppStates, + DependencyStates: a.DependencyStates, + DnsRecords: a.DNSRecords, + }, nil +} + func (p *Plugin) Apply(r *apiv1.ApplyRequest, reg *registry.Registry, stream apiv1.DeployPluginService_ApplyServer) error { a, err := actions.NewPlan(p.PluginContext(), p.log, r.State, r.Domains, reg, r.Destroy, false) if err != nil { diff --git a/plugin/monitoring.go b/plugin/monitoring.go new file mode 100644 index 0000000..f69f4c8 --- /dev/null +++ b/plugin/monitoring.go @@ -0,0 +1,193 @@ +package plugin + +import ( + "context" + "fmt" + + "github.com/outblocks/cli-plugin-gcp/gcp" + plugin_go "github.com/outblocks/outblocks-plugin-go" + apiv1 "github.com/outblocks/outblocks-plugin-go/gen/api/v1" + "github.com/outblocks/outblocks-plugin-go/registry" + "github.com/outblocks/outblocks-plugin-go/registry/fields" + "github.com/outblocks/outblocks-plugin-go/types" +) + +func prepareRegistry(reg *registry.Registry, data []byte) error { + gcp.RegisterTypes(reg) + + return reg.Load(data) +} + +func (p *Plugin) registerMonitoring(reg *registry.Registry, data *apiv1.MonitoringData) error { + var ( + checks []*gcp.UptimeCheckConfig + channels []*gcp.NotificationChannel + ) + + for _, target := range data.Targets { + check := &gcp.UptimeCheckConfig{ + DisplayName: fields.String(fmt.Sprintf("Outblock Uptime Check for %s/%s: %s", + p.env.Env(), p.env.ProjectName(), target.Url)), + ProjectID: fields.String(p.settings.ProjectID), + URL: fields.String(target.Url), + Frequency: fields.Int(int(target.Frequency)), + } + + checks = append(checks, check) + + _, err := reg.RegisterPluginResource("uptime check", target.Url, check) + if err != nil { + return err + } + } + + for _, ch := range data.Channels { + labels := make(map[string]fields.Field) + chID := "" + + switch ch.Type { + case "slack": + obj, err := types.NewMonitoringChannelSlack(ch.Properties.AsMap()) + if err != nil { + return err + } + + labels["channel_name"] = fields.String(obj.Channel) + labels["auth_token"] = fields.String(obj.Token) + chID = fmt.Sprintf("slack:%s", obj.Channel) + + case "email": + obj, err := types.NewMonitoringChannelEmail(ch.Properties.AsMap()) + if err != nil { + return err + } + + labels["email_address"] = fields.String(obj.Email) + chID = fmt.Sprintf("email:%s", obj.Email) + default: + continue + } + + channel := &gcp.NotificationChannel{ + DisplayName: fields.String(fmt.Sprintf("Outblocks Notification for %s/%s: %s", + p.env.Env(), p.env.ProjectName(), chID)), + ProjectID: fields.String(p.settings.ProjectID), + Type: fields.String(ch.Type), + Labels: fields.Map(labels), + } + + channels = append(channels, channel) + + _, err := reg.RegisterPluginResource("notification channel", chID, channel) + if err != nil { + return err + } + } + + if len(channels) != 0 { + chIDs := make([]fields.Field, len(channels)) + for i, ch := range channels { + chIDs[i] = ch.ID.Input() + } + + for _, t := range checks { + _, err := reg.RegisterPluginResource("uptime alert", t.URL.Wanted(), &gcp.UptimeAlertPolicy{ + DisplayName: fields.String(fmt.Sprintf("Outblocks Uptime Alert for %s/%s: %s", + p.env.Env(), p.env.ProjectName(), t.URL.Wanted())), + ProjectID: t.ProjectID, + CheckID: t.ID.Input(), + NotificationChannelIDs: fields.Array(chIDs), + }) + if err != nil { + return err + } + } + } + + return nil +} + +func (p *Plugin) PlanMonitoring(ctx context.Context, reg *registry.Registry, r *apiv1.PlanMonitoringRequest) (*apiv1.PlanMonitoringResponse, error) { + if r.State.Other == nil { + r.State.Other = make(map[string][]byte) + } + + reg = reg.Partition("monitoring") + pctx := p.PluginContext() + state := r.State + monitoring := r.Data + + err := prepareRegistry(reg, r.State.Registry) + if err != nil { + return nil, err + } + + // Register monitoring objects. + err = p.registerMonitoring(reg, monitoring) + if err != nil { + return nil, err + } + + // Process registry. + diff, err := reg.ProcessAndDiff(ctx, pctx) + if err != nil { + return nil, err + } + + data, err := reg.Dump() + if err != nil { + return nil, err + } + + r.State.Registry = data + + return &apiv1.PlanMonitoringResponse{ + Plan: &apiv1.Plan{ + Actions: registry.PlanActionFromDiff(diff), + }, + State: state, + }, nil +} + +func (p *Plugin) ApplyMonitoring(r *apiv1.ApplyMonitoringRequest, reg *registry.Registry, stream apiv1.MonitoringPluginService_ApplyMonitoringServer) error { + reg = reg.Partition("monitoring") + ctx := stream.Context() + pctx := p.PluginContext() + monitoring := r.Data + + err := prepareRegistry(reg, r.State.Registry) + if err != nil { + return err + } + + // Register monitoring objects. + err = p.registerMonitoring(reg, monitoring) + if err != nil { + return err + } + + // Process registry. + diff, err := reg.ProcessAndDiff(ctx, pctx) + if err != nil { + return err + } + + err = reg.Apply(ctx, pctx, diff, plugin_go.DefaultRegistryApplyMonitoringCallback(stream)) + + data, saveErr := reg.Dump() + if err == nil { + err = saveErr + } + + r.State.Registry = data + + _ = stream.Send(&apiv1.ApplyMonitoringResponse{ + Response: &apiv1.ApplyMonitoringResponse_Done{ + Done: &apiv1.ApplyMonitoringDoneResponse{ + State: r.State, + }, + }, + }) + + return err +} diff --git a/plugin/plan.go b/plugin/plan.go deleted file mode 100644 index 5174ed7..0000000 --- a/plugin/plan.go +++ /dev/null @@ -1,30 +0,0 @@ -package plugin - -import ( - "context" - - "github.com/outblocks/cli-plugin-gcp/actions" - apiv1 "github.com/outblocks/outblocks-plugin-go/gen/api/v1" - "github.com/outblocks/outblocks-plugin-go/registry" -) - -func (p *Plugin) Plan(ctx context.Context, reg *registry.Registry, r *apiv1.PlanRequest) (*apiv1.PlanResponse, error) { - a, err := actions.NewPlan(p.PluginContext(), p.log, r.State, r.Domains, reg, r.Destroy, false) - if err != nil { - return nil, err - } - - deployPlan, err := a.Plan(ctx, r.Apps, r.Dependencies) - if err != nil { - return nil, err - } - - return &apiv1.PlanResponse{ - Deploy: deployPlan, - - State: a.State, - AppStates: a.AppStates, - DependencyStates: a.DependencyStates, - DnsRecords: a.DNSRecords, - }, nil -} diff --git a/plugin/plugin.go b/plugin/plugin.go index 59797a2..98bd568 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -83,10 +83,11 @@ func (p *Plugin) runAndEnsureAPI(ctx context.Context, f func() error) error { } var ( - _ plugin.LockingPluginHandler = (*Plugin)(nil) - _ plugin.StatePluginHandler = (*Plugin)(nil) - _ plugin.DeployPluginHandler = (*Plugin)(nil) - _ plugin.CommandPluginHandler = (*Plugin)(nil) - _ plugin.LogsPluginHandler = (*Plugin)(nil) - _ plugin.SecretPluginHandler = (*Plugin)(nil) + _ plugin.LockingPluginHandler = (*Plugin)(nil) + _ plugin.StatePluginHandler = (*Plugin)(nil) + _ plugin.DeployPluginHandler = (*Plugin)(nil) + _ plugin.CommandPluginHandler = (*Plugin)(nil) + _ plugin.LogsPluginHandler = (*Plugin)(nil) + _ plugin.SecretPluginHandler = (*Plugin)(nil) + _ plugin.MonitoringPluginHandler = (*Plugin)(nil) )