Skip to content

Commit

Permalink
feat: Add support for export to MonitoringDashboard direct actuation
Browse files Browse the repository at this point in the history
  • Loading branch information
justinsb committed Jun 19, 2024
1 parent aa7eb04 commit cc62254
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 30 deletions.
77 changes: 77 additions & 0 deletions mockgcp/mockmonitoring/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
pb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/mockgcp/monitoring/dashboard/v1"
Expand Down Expand Up @@ -67,6 +68,12 @@ func (s *DashboardsService) CreateDashboard(ctx context.Context, req *pb.CreateD
defaulter := &dashboardDefaulter{}
defaulter.visitDashboard(obj)

validator := &dashboardValidator{}
validator.visitDashboard(obj)
if len(validator.errors) > 0 {
return nil, status.Errorf(codes.InvalidArgument, "%v", validator.errors[0])
}

obj.Name = fqn
obj.Etag = computeEtag(obj)

Expand Down Expand Up @@ -131,6 +138,70 @@ func (d *dashboardDefaulter) visitTextWidget(obj *pb.Widget_Text) {
}
}

type dashboardValidator struct {
errors []error
}

func (d *dashboardValidator) errorf(format string, args ...interface{}) {
d.errors = append(d.errors, fmt.Errorf(format, args...))
}

func (d *dashboardValidator) visitDashboard(obj *pb.Dashboard) {
switch layout := obj.Layout.(type) {
case *pb.Dashboard_ColumnLayout:
d.visitColumnLayout(layout.ColumnLayout)
}
}

func (d *dashboardValidator) visitColumnLayout(obj *pb.ColumnLayout) {
for _, column := range obj.Columns {
for _, widget := range column.Widgets {
d.visitWidget(widget)
}
}
}

func (d *dashboardValidator) visitWidget(obj *pb.Widget) {
switch content := obj.Content.(type) {
case *pb.Widget_XyChart:
d.visitXYChartWidget(content.XyChart)

case *pb.Widget_Scorecard:
d.visitScorecardWidget(content)
case *pb.Widget_Text:
d.visitTextWidget(content)
}
}

func formatDuration(d *durationpb.Duration) string {
return fmt.Sprintf("%ds", d.Seconds)
}

func (d *dashboardValidator) visitXYChartWidget(obj *pb.XyChart) {
timeshiftDuration := obj.TimeshiftDuration
if timeshiftDuration != nil && timeshiftDuration.AsDuration() != 0 {
if timeshiftDuration.Seconds < 60 {
// Should be columnLayout.columns[0].widgets[0].xyChart.timeshiftDuration ...
d.errorf("Field columnLayout.columns[].widgets[].xyChart.timeshiftDuration has an invalid value of %q: must be greater than or equal to one minute.", formatDuration(timeshiftDuration))
return
}

for _, dataSet := range obj.DataSets {
switch dataSet.GetPlotType() {
case pb.XyChart_DataSet_STACKED_BAR:
// TODO: Should be Field columnLayout.columns[0].widgets[2].xyChart.dataSets[0].plotType ...
d.errorf("Field columnLayout.columns[].widgets[].xyChart.dataSets[].plotType has an invalid value of %q: plot type is incompatible with XyChart's timeshiftDuration.", dataSet.GetPlotType())
}
}
}
}

func (d *dashboardValidator) visitScorecardWidget(obj *pb.Widget_Scorecard) {
}

func (d *dashboardValidator) visitTextWidget(obj *pb.Widget_Text) {
}

func (s *DashboardsService) UpdateDashboard(ctx context.Context, req *pb.UpdateDashboardRequest) (*pb.Dashboard, error) {
name, err := s.parseDashboardName(req.GetDashboard().GetName())
if err != nil {
Expand All @@ -153,6 +224,12 @@ func (s *DashboardsService) UpdateDashboard(ctx context.Context, req *pb.UpdateD
defaulter := &dashboardDefaulter{}
defaulter.visitDashboard(updated)

validator := &dashboardValidator{}
validator.visitDashboard(updated)
if len(validator.errors) > 0 {
return nil, status.Errorf(codes.InvalidArgument, "%v", validator.errors[0])
}

updated.Name = fqn
updated.Etag = computeEtag(updated)

Expand Down
36 changes: 36 additions & 0 deletions pkg/controller/direct/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,41 @@ func Export(ctx context.Context, url string, config *config.ControllerConfig) (*
return u, nil
}
}

//monitoring.googleapis.com/projects/PROJECT_NUMBER/dashboards/DASHBOARD_ID
if strings.HasPrefix(url, "//monitoring.googleapis.com/") {
tokens := strings.Split(strings.TrimPrefix(url, "//monitoring.googleapis.com/"), "/")
if len(tokens) == 4 && tokens[0] == "projects" && tokens[2] == "dashboards" {
model, err := registry.GetModel(schema.GroupKind{Group: "monitoring.cnrm.cloud.google.com", Kind: "MonitoringDashboard"})
if err != nil {
return nil, err
}
in := &unstructured.Unstructured{}
in.SetName(tokens[3])
if err := unstructured.SetNestedField(in.Object, tokens[1], "spec", "projectRef", "external"); err != nil {
return nil, err
}

var reader client.Reader // TODO: Create erroring reader?
a, err := model.AdapterForObject(ctx, reader, in)
if err != nil {
return nil, err
}
found, err := a.Find(ctx)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("resource %q is not found", url)
}

u, err := a.Export(ctx)
if err != nil {
return nil, err
}

return u, nil
}
}
return nil, nil
}
20 changes: 7 additions & 13 deletions pkg/controller/direct/monitoring/maputils.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,33 +123,27 @@ func Duration_ToProto(mapCtx *MapContext, in *string) *durationpb.Duration {
return nil
}

if strings.HasPrefix(s, "seconds:") {
v := strings.TrimPrefix(s, "seconds:")
d, err := time.ParseDuration(v + "s")
if strings.HasSuffix(s, "s") {
d, err := time.ParseDuration(s)
if err != nil {
mapCtx.Errorf("parsing duration %q: %w", v, err)
mapCtx.Errorf("parsing duration %q: %w", s, err)
return nil
}
out := durationpb.New(d)
return out
}

// TODO: Is this 1:1 with durationpb?
d, err := time.ParseDuration(s)
if err != nil {
mapCtx.Errorf("parsing duration %q: %w", s, err)
return nil
}
out := durationpb.New(d)
return out
mapCtx.Errorf("parsing duration %q, must end in s", s)
return nil
}

func Duration_FromProto(mapCtx *MapContext, in *durationpb.Duration) *string {
if in == nil {
return nil
}

s := in.String()
d := in.AsDuration()
s := fmt.Sprintf("%vs", d.Seconds())
return &s
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ func (a *dashboardAdapter) Export(ctx context.Context) (*unstructured.Unstructur
return nil, fmt.Errorf("error converting dashboard from API %w", err)
}

spec.ProjectRef.External = a.projectID

specObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(spec)
if err != nil {
return nil, fmt.Errorf("error converting dashboard spec to unstructured: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/direct/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func SupportsIAM(groupKind schema.GroupKind) (bool, error) {
switch groupKind {
case schema.GroupKind{Group: "logging.cnrm.cloud.google.com", Kind: "LoggingLogMetric"}:
return false, nil
case schema.GroupKind{Group: "monitoring.cnrm.cloud.google.com", Kind: "MonitoringDashboard"}:
return false, nil
}
return false, fmt.Errorf("groupKind %v is not recognized as a direct kind", groupKind)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
apiVersion: monitoring.cnrm.cloud.google.com/v1beta1
kind: MonitoringDashboard
metadata:
name: monitoringdashboard-${uniqueId}
spec:
columnLayout:
columns:
- weight: 2
widgets:
- title: Widget 1
xyChart:
dataSets:
- plotType: LINE
timeSeriesQuery:
timeSeriesFilter:
aggregation:
perSeriesAligner: ALIGN_RATE
filter: metric.type="agent.googleapis.com/nginx/connections/accepted_count"
unitOverride: "1"
timeshiftDuration: 0s
yAxis:
label: y1Axis
scale: LINEAR
- text:
content: Widget 2
format: MARKDOWN
- title: Widget 3
xyChart:
dataSets:
- plotType: STACKED_BAR
timeSeriesQuery:
timeSeriesFilter:
aggregation:
perSeriesAligner: ALIGN_RATE
filter: metric.type="agent.googleapis.com/nginx/connections/accepted_count"
unitOverride: "1"
timeshiftDuration: ""
yAxis:
label: y1Axis
scale: LINEAR
- logsPanel:
filter: metric.type="agent.googleapis.com/nginx/connections/accepted_count"
title: Widget 4
displayName: monitoringdashboard-updated
projectRef:
external: ${projectId}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
apiVersion: monitoring.cnrm.cloud.google.com/v1beta1
kind: MonitoringDashboard
metadata:
name: monitoringdashboard-${uniqueId}
spec:
columnLayout:
columns:
- weight: 2
widgets:
- title: Widget 1
xyChart:
dataSets:
- plotType: LINE
timeSeriesQuery:
timeSeriesFilter:
aggregation:
perSeriesAligner: ALIGN_RATE
filter: metric.type="agent.googleapis.com/nginx/connections/accepted_count"
unitOverride: "1"
timeshiftDuration: 0s
yAxis:
label: y1Axis
scale: LINEAR
- text:
content: Widget 2
format: MARKDOWN
- title: Widget 3
xyChart:
dataSets:
- plotType: STACKED_BAR
timeSeriesQuery:
timeSeriesFilter:
aggregation:
perSeriesAligner: ALIGN_RATE
filter: metric.type="agent.googleapis.com/nginx/connections/accepted_count"
unitOverride: "1"
timeshiftDuration: ""
yAxis:
label: y1Axis
scale: LINEAR
- logsPanel:
filter: metric.type="agent.googleapis.com/nginx/connections/accepted_count"
title: Widget 4
displayName: monitoringdashboard updated
projectRef:
external: other${uniqueId}
62 changes: 45 additions & 17 deletions tests/e2e/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,44 +22,35 @@ import (
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/cmd/export"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
)

func exportResource(h *create.Harness, obj *unstructured.Unstructured) string {
exportURI := ""

projectID := obj.GetAnnotations()["cnrm.cloud.google.com/project-id"]
if projectID == "" {
projectID = h.Project.ProjectID
}
projectID := resolveProjectID(h, obj)

resourceID, _, _ := unstructured.NestedString(obj.Object, "spec", "resourceID")
if resourceID == "" {
resourceID = obj.GetName()
}
// location, _, _ := unstructured.NestedString(obj.Object, "spec", "location")

// This list should match https://cloud.google.com/asset-inventory/docs/resource-name-format
gvk := obj.GroupVersionKind()
switch gvk.GroupKind() {
case schema.GroupKind{Group: "serviceusage.cnrm.cloud.google.com", Kind: "Service"}:
exportURI = "//serviceusage.googleapis.com/projects/" + projectID + "/services/" + resourceID
// case schema.GroupKind{Group: "certificatemanager.cnrm.cloud.google.com", Kind: "CertificateManagerCertificate"}:
// exportURI = "//certificatemanager.googleapis.com/projects/" + projectID + "/locations/" + location + "/certificates/" + resourceID
// case schema.GroupKind{Group: "certificatemanager.cnrm.cloud.google.com", Kind: "CertificateManagerCertificateMap"}:
// if location == "" {
// location = "global"
// }
// exportURI = "//certificatemanager.googleapis.com/projects/" + projectID + "/locations/" + location + "/certificateMaps/" + resourceID
// case schema.GroupKind{Group: "certificatemanager.cnrm.cloud.google.com", Kind: "CertificateManagerCertificateMapEntry"}:
// exportURI = "//certificatemanager.googleapis.com/projects/" + projectID + "/locations/" + location + "/certificateMaps/" + certificateMapID + "/certificateMapEntries/" + resourceID
// TODO: This does not work
// case schema.GroupKind{Group: "iam.cnrm.cloud.google.com", Kind: "IAMServiceAccount"}:
// name := obj.GetName()
// exportURI = "//iam.googleapis.com/projects/" + projectID + "/serviceAccounts/" + name

case schema.GroupKind{Group: "bigquery.cnrm.cloud.google.com", Kind: "BigQueryDataset"}:
exportURI = "//bigquery.googleapis.com/projects/" + projectID + "/datasets/" + resourceID

case schema.GroupKind{Group: "logging.cnrm.cloud.google.com", Kind: "LoggingLogMetric"}:
exportURI = "//logging.googleapis.com/projects/" + projectID + "/metrics/" + resourceID

case schema.GroupKind{Group: "monitoring.cnrm.cloud.google.com", Kind: "MonitoringDashboard"}:
exportURI = "//monitoring.googleapis.com/projects/" + projectID + "/dashboards/" + resourceID
}

if exportURI == "" {
Expand Down Expand Up @@ -96,3 +87,40 @@ func exportResourceAsUnstructured(h *create.Harness, obj *unstructured.Unstructu
}
return u
}

func resolveProjectID(h *create.Harness, obj *unstructured.Unstructured) string {
projectRefExternal, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "external")
if projectRefExternal != "" {
return projectRefExternal
}

projectRefName, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "name")
if projectRefName != "" {
projectRefNamespace, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "namespace")

project := &unstructured.Unstructured{}
project.SetGroupVersionKind(schema.GroupVersionKind{Group: "resourcemanager.cnrm.cloud.google.com", Version: "v1beta1", Kind: "Project"})
projectKey := types.NamespacedName{
Name: projectRefName,
Namespace: projectRefNamespace,
}
if projectKey.Namespace == "" {
projectKey.Namespace = obj.GetNamespace()
}
if err := h.GetClient().Get(h.Ctx, projectKey, project); err != nil {
h.Fatalf("resolving projectRef: %v", err)
}
projectID, _, _ := unstructured.NestedString(project.Object, "spec", "resourceID")
if projectID == "" {
projectID = obj.GetName()
}
return projectID
}

if projectID := obj.GetAnnotations()["cnrm.cloud.google.com/project-id"]; projectID != "" {
return projectID
}

// Assume it's the namespace
return h.Project.ProjectID
}

0 comments on commit cc62254

Please sign in to comment.