diff --git a/pkg/controller/direct/monitoring/client.go b/pkg/controller/direct/monitoring/client.go new file mode 100644 index 0000000000..50b1b7777e --- /dev/null +++ b/pkg/controller/direct/monitoring/client.go @@ -0,0 +1,88 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package monitoring + +import ( + "context" + "fmt" + "net/http" + + api "cloud.google.com/go/monitoring/dashboard/apiv1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" + "google.golang.org/api/option" +) + +type gcpClient struct { + config config.ControllerConfig +} + +func newGCPClient(ctx context.Context, config *config.ControllerConfig) (*gcpClient, error) { + gcpClient := &gcpClient{ + config: *config, + } + return gcpClient, nil +} + +func (m *gcpClient) options() ([]option.ClientOption, error) { + var opts []option.ClientOption + if m.config.UserAgent != "" { + opts = append(opts, option.WithUserAgent(m.config.UserAgent)) + } + if m.config.HTTPClient != nil { + // TODO: Set UserAgent in this scenario (error is: WithHTTPClient is incompatible with gRPC dial options) + + httpClient := &http.Client{} + *httpClient = *m.config.HTTPClient + httpClient.Transport = &optionsRoundTripper{ + config: m.config, + inner: m.config.HTTPClient.Transport, + } + opts = append(opts, option.WithHTTPClient(httpClient)) + } + if m.config.UserProjectOverride && m.config.BillingProject != "" { + opts = append(opts, option.WithQuotaProject(m.config.BillingProject)) + } + + // TODO: support endpoints? + // if m.config.Endpoint != "" { + // opts = append(opts, option.WithEndpoint(m.config.Endpoint)) + // } + + return opts, nil +} + +type optionsRoundTripper struct { + config config.ControllerConfig + inner http.RoundTripper +} + +func (m *optionsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if m.config.UserAgent != "" { + req.Header.Set("User-Agent", m.config.UserAgent) + } + return m.inner.RoundTrip(req) +} + +func (m *gcpClient) newDashboardsClient(ctx context.Context) (*api.DashboardsClient, error) { + opts, err := m.options() + if err != nil { + return nil, err + } + client, err := api.NewDashboardsRESTClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("building dashboard client: %w", err) + } + return client, err +} diff --git a/pkg/controller/direct/monitoring/monitoringdashboard_controller.go b/pkg/controller/direct/monitoring/monitoringdashboard_controller.go new file mode 100644 index 0000000000..0330ddc2c7 --- /dev/null +++ b/pkg/controller/direct/monitoring/monitoringdashboard_controller.go @@ -0,0 +1,281 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package monitoring + +import ( + "context" + "fmt" + + api "cloud.google.com/go/monitoring/dashboard/apiv1" + pb "cloud.google.com/go/monitoring/dashboard/apiv1/dashboardpb" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/monitoring/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/references" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" +) + +func init() { + registry.RegisterModel(krm.MonitoringDashboardGVK, newDashboardModel) +} + +func newDashboardModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) { + return &dashboardModel{config: config}, nil +} + +type dashboardModel struct { + config *config.ControllerConfig +} + +// model implements the Model interface. +var _ directbase.Model = &dashboardModel{} + +type dashboardAdapter struct { + projectID string + resourceID string + + desired *pb.Dashboard + actual *pb.Dashboard + + dashboardsClient *api.DashboardsClient +} + +// adapter implements the Adapter interface. +var _ directbase.Adapter = &dashboardAdapter{} + +// AdapterForObject implements the Model interface. +func (m *dashboardModel) AdapterForObject(ctx context.Context, kube client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) { + gcpClient, err := newGCPClient(ctx, m.config) + if err != nil { + return nil, fmt.Errorf("building gcp client: %w", err) + } + + dashboardsClient, err := gcpClient.newDashboardsClient(ctx) + if err != nil { + return nil, err + } + + obj := &krm.MonitoringDashboard{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { + return nil, fmt.Errorf("error converting to %T: %w", obj, err) + } + + resourceID := ValueOf(obj.Spec.ResourceID) + if resourceID == "" { + resourceID = obj.GetName() + } + if resourceID == "" { + return nil, fmt.Errorf("cannot resolve resource ID") + } + + projectRef, err := references.ResolveProject(ctx, kube, obj, &obj.Spec.ProjectRef) + if err != nil { + return nil, err + } + projectID := projectRef.ProjectID + if projectID == "" { + return nil, fmt.Errorf("cannot resolve project") + } + + if err := VisitFields(obj, &refNormalizer{ctx: ctx, src: obj, kube: kube}); err != nil { + return nil, err + } + + mapCtx := &MapContext{} + desiredProto := MonitoringDashboardSpec_ToProto(mapCtx, &obj.Spec) + if mapCtx.Err() != nil { + return nil, mapCtx.Err() + } + + return &dashboardAdapter{ + projectID: projectID, + resourceID: resourceID, + desired: desiredProto, + dashboardsClient: dashboardsClient, + }, nil +} + +// Find implements the Adapter interface. +func (a *dashboardAdapter) Find(ctx context.Context) (bool, error) { + if a.resourceID == "" { + return false, nil + } + + req := &pb.GetDashboardRequest{ + Name: a.fullyQualifiedName(), + } + dashboard, err := a.dashboardsClient.GetDashboard(ctx, req) + if err != nil { + if IsNotFound(err) { + return false, nil + } + return false, err + } + + a.actual = dashboard + + return true, nil +} + +// Delete implements the Adapter interface. +func (a *dashboardAdapter) Delete(ctx context.Context) (bool, error) { + // Check if exists / already deleted + // Technically we can just delete, but this is a little cleaner in logs etc. + exists, err := a.Find(ctx) + if err != nil { + return false, err + } + if !exists { + return false, nil + } + + // TODO: Delete via status selfLink? + req := &pb.DeleteDashboardRequest{ + Name: a.fullyQualifiedName(), + } + + if err := a.dashboardsClient.DeleteDashboard(ctx, req); err != nil { + if IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("deleting dashboard %s: %w", a.fullyQualifiedName(), err) + } + + return true, nil +} + +// Create implements the Adapter interface. +func (a *dashboardAdapter) Create(ctx context.Context, u *unstructured.Unstructured) error { + log := klog.FromContext(ctx) + log.V(2).Info("creating object", "u", u) + + parent := "projects/" + a.projectID + + req := &pb.CreateDashboardRequest{ + Parent: parent, + Dashboard: a.desired, + } + req.Dashboard.Name = a.fullyQualifiedName() + + log.V(2).Info("creating dashboard", "req", req) + created, err := a.dashboardsClient.CreateDashboard(ctx, req) + if err != nil { + return fmt.Errorf("creating dashboard: %w", err) + } + log.V(2).Info("created dashboard", "dashboard", created) + + resourceID := lastComponent(created.Name) + if err := unstructured.SetNestedField(u.Object, resourceID, "spec", "resourceID"); err != nil { + return fmt.Errorf("setting spec.resourceID: %w", err) + } + + mapCtx := &MapContext{} + status := MonitoringDashboardStatus_FromProto(mapCtx, created) + if mapCtx.Err() != nil { + return mapCtx.Err() + } + return setStatus(u, status) +} + +// Update implements the Adapter interface. +func (a *dashboardAdapter) Update(ctx context.Context, u *unstructured.Unstructured) error { + log := klog.FromContext(ctx) + log.V(2).Info("updating object", "u", u) + + // TODO: Where/how do we want to enforce immutability? + + changedFields := ComputeChangedFields(onlySpec(a.desired), onlySpec(a.actual)) + if len(changedFields) != 0 { + log.Info("changed fields", "fields", sets.List(changedFields)) + + req := &pb.UpdateDashboardRequest{ + Dashboard: a.desired, + } + req.Dashboard.Name = a.fullyQualifiedName() + + log.V(2).Info("updating dashboard", "request", req) + updated, err := a.dashboardsClient.UpdateDashboard(ctx, req) + if err != nil { + return err + } + log.V(2).Info("updated dashboard", "dashboard", updated) + a.actual = updated + } + + mapCtx := &MapContext{} + status := MonitoringDashboardStatus_FromProto(mapCtx, a.actual) + if mapCtx.Err() != nil { + return mapCtx.Err() + } + return setStatus(u, status) +} + +func (a *dashboardAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) { + if a.actual == nil { + return nil, fmt.Errorf("dashboard %q not found", a.fullyQualifiedName()) + } + + mc := &MapContext{} + spec := MonitoringDashboardSpec_FromProto(mc, a.actual) + if err := mc.Err(); err != nil { + return nil, fmt.Errorf("error converting dashboard from API %w", err) + } + + specObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(spec) + if err != nil { + return nil, fmt.Errorf("error converting dashboard spec to unstructured: %w", err) + } + + u := &unstructured.Unstructured{ + Object: make(map[string]interface{}), + } + u.SetName(a.resourceID) + u.SetGroupVersionKind(krm.MonitoringDashboardGVK) + if err := unstructured.SetNestedField(u.Object, specObj, "spec"); err != nil { + return nil, fmt.Errorf("setting spec: %w", err) + } + + return u, nil +} + +func onlySpec(in *pb.Dashboard) *pb.Dashboard { + // We could also do this "directly" with... + // c := proto.Clone(in).(*pb.Dashboard) + // c.Etag = "" + // c.Name = "" + + // Remove unmapped fields by round-tripping through spec + mapCtx := &MapContext{} + spec := MonitoringDashboardSpec_FromProto(mapCtx, in) + if mapCtx.Err() != nil { + klog.Fatalf("error during onlySpec: %v", mapCtx.Err()) + } + + out := MonitoringDashboardSpec_ToProto(mapCtx, spec) + if mapCtx.Err() != nil { + klog.Fatalf("error during onlySpec: %v", mapCtx.Err()) + } + return out +} + +func (a *dashboardAdapter) fullyQualifiedName() string { + return fmt.Sprintf("projects/%s/dashboards/%s", a.projectID, a.resourceID) +} diff --git a/pkg/controller/direct/monitoring/refs.go b/pkg/controller/direct/monitoring/refs.go new file mode 100644 index 0000000000..8ddd53dd96 --- /dev/null +++ b/pkg/controller/direct/monitoring/refs.go @@ -0,0 +1,105 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package monitoring + +import ( + "context" + "fmt" + "strings" + + krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/monitoring/v1beta1" + refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/references" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func normalizeResourceName(ctx context.Context, reader client.Reader, src client.Object, ref *v1alpha1.ResourceRef) (*v1alpha1.ResourceRef, error) { + if ref == nil { + return nil, nil + } + + // For backwards compatibility, infer "Project" kind + if ref.Kind == "" && ref.External != "" { + tokens := strings.Split(ref.External, "/") + if len(tokens) == 2 && tokens[0] == "projects" { + ref.Kind = "Project" + } + } + + if ref.Kind == "" { + return nil, fmt.Errorf("must specify kind on reference (%+v)", ref) + } + if ref.Name == "" && ref.External == "" { + return nil, fmt.Errorf("must specify either name or external on reference") + } + if ref.Name != "" && ref.External != "" { + return nil, fmt.Errorf("cannot specify both name and external on reference") + } + + switch ref.Kind { + case "Project": + project, err := references.ResolveProject(ctx, reader, src, &refs.ProjectRef{ + Name: ref.Name, + Namespace: ref.Namespace, + External: ref.External, + Kind: ref.Kind, + }) + if err != nil { + return nil, err + } + + ref = &v1alpha1.ResourceRef{ + Kind: ref.Kind, + External: fmt.Sprintf("projects/%s", project.ProjectID), + } + + default: + return nil, fmt.Errorf("references to kind %q are not supported", ref.Kind) + } + + tokens := strings.Split(ref.External, "/") + switch ref.Kind { + case "Project": + if len(tokens) == 2 && tokens[0] == "projects" { + // OK + } else { + return nil, fmt.Errorf("resourceName %q should be in the format projects/", ref.External) + } + default: + return nil, fmt.Errorf("references to kind %q are not supported", ref.Kind) + } + + return ref, nil +} + +type refNormalizer struct { + ctx context.Context + kube client.Reader + src client.Object +} + +func (r *refNormalizer) VisitField(path string, v any) error { + if logsPanel, ok := v.(*krm.LogsPanel); ok { + for i := range logsPanel.ResourceNames { + if ref, err := normalizeResourceName(r.ctx, r.kube, r.src, &logsPanel.ResourceNames[i]); err != nil { + return err + } else { + logsPanel.ResourceNames[i] = *ref + } + } + } + return nil +} diff --git a/pkg/controller/direct/monitoring/roundtrip_test.go b/pkg/controller/direct/monitoring/roundtrip_test.go index 9cbbba6169..456c593945 100644 --- a/pkg/controller/direct/monitoring/roundtrip_test.go +++ b/pkg/controller/direct/monitoring/roundtrip_test.go @@ -33,7 +33,7 @@ import ( // IDEA: Load all the samples, and check that we have all the KRM paths covered -func FuzzFromProto(f *testing.F) { +func FuzzMonitoringDashboardSpec(f *testing.F) { f.Fuzz(func(t *testing.T, seed int64) { randStream := rand.New(rand.NewSource(seed)) diff --git a/pkg/controller/direct/monitoring/utils.go b/pkg/controller/direct/monitoring/utils.go index f9889a247c..c01ea172de 100644 --- a/pkg/controller/direct/monitoring/utils.go +++ b/pkg/controller/direct/monitoring/utils.go @@ -14,6 +14,23 @@ package monitoring +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/gax-go/v2/apierror" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/testing/protocmp" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" +) + func ValueOf[T any](p *T) T { var v T if p != nil { @@ -25,3 +42,142 @@ func ValueOf[T any](p *T) T { func PtrTo[T any](t T) *T { return &t } + +// IsNotFound returns true if the given error is an HTTP 404. +func IsNotFound(err error) bool { + return HasHTTPCode(err, 404) +} + +// HasHTTPCode returns true if the given error is an HTTP response with the given code. +func HasHTTPCode(err error, code int) bool { + if err == nil { + return false + } + apiError := &apierror.APIError{} + if errors.As(err, &apiError) { + if apiError.HTTPCode() == code { + return true + } + } else { + klog.Warningf("unexpected error type %T", err) + } + return false +} + +func lastComponent(s string) string { + i := strings.LastIndex(s, "/") + return s[i+1:] +} + +func ComputeChangedFields(actual proto.Message, desired proto.Message) sets.Set[string] { + changes := sets.New[string]() + actualReflect := actual.ProtoReflect() + desiredReflect := desired.ProtoReflect() + actualReflect.Range(func(field protoreflect.FieldDescriptor, actualValue protoreflect.Value) bool { + desiredValue := desiredReflect.Get(field) + if !actualValue.Equal(desiredValue) { + changes.Insert(field.JSONName()) + } + return true + }) + desiredReflect.Range(func(field protoreflect.FieldDescriptor, desiredValue protoreflect.Value) bool { + actualValue := actualReflect.Get(field) + if !actualValue.Equal(desiredValue) { + changes.Insert(field.JSONName()) + } + return true + }) + if changes.Len() != 0 { + klog.V(2).Infof("ComputeChangedFields found diff fields=%v, diff=%v", sets.List(changes), cmp.Diff(actual, desired, protocmp.Transform())) + } + return changes +} + +func setStatus(u *unstructured.Unstructured, typedStatus any) error { + status, err := runtime.DefaultUnstructuredConverter.ToUnstructured(typedStatus) + if err != nil { + return fmt.Errorf("error converting status to unstructured: %w", err) + } + + // Use existing values for conditions/observedGeneration; they are managed in k8s not the GCP API + old, _, _ := unstructured.NestedMap(u.Object, "status") + if old != nil { + status["conditions"] = old["conditions"] + status["observedGeneration"] = old["observedGeneration"] + } + + u.Object["status"] = status + + return nil +} + +type Visitor interface { + VisitField(path string, value any) error +} + +func VisitFields(obj any, visitor Visitor) error { + w := &visitorWalker{visitor: visitor} + w.visitAny("", reflect.ValueOf(obj)) + return errors.Join(w.errs...) +} + +type visitorWalker struct { + visitor Visitor + errs []error +} + +func (w *visitorWalker) visitAny(path string, v reflect.Value) { + shouldCallVisitor := true + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + // Skip nil pointers + shouldCallVisitor = false + } + } + if shouldCallVisitor { + if err := w.visitor.VisitField(path, v.Interface()); err != nil { + w.errs = append(w.errs, err) + } + } + + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return + } + w.visitAny(path, v.Elem()) + + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + if field.IsExported() { + fieldName := field.Name + w.visitAny(path+"."+fieldName, v.Field(i)) + } + } + + case reflect.Map: + for _, key := range v.MapKeys() { + w.visitAny(path+"."+key.String(), v.MapIndex(key)) + } + + case reflect.Slice: + elemType := v.Type().Elem() + switch elemType.Kind() { + case reflect.Struct, reflect.String: + for i := 0; i < v.Len(); i++ { + w.visitAny(path+"[]", v.Index(i)) + } + case reflect.Uint8: + // Do not visit []byte as individual values, treat as a leaf + default: + w.errs = append(w.errs, fmt.Errorf("visiting slice of type %v is not supported", elemType.Kind())) + } + + case reflect.String, reflect.Bool, reflect.Int32, reflect.Int64, reflect.Float64: + // "leaf", nothing to recurse into + default: + w.errs = append(w.errs, fmt.Errorf("visiting type %v is not supported", v.Kind())) + } +} diff --git a/pkg/controller/direct/references/projectref.go b/pkg/controller/direct/references/projectref.go index 3b9d8e2448..435b46d263 100644 --- a/pkg/controller/direct/references/projectref.go +++ b/pkg/controller/direct/references/projectref.go @@ -31,6 +31,7 @@ type Project struct { ProjectID string } +// ResolveProject will resolve a ProjectRef to a Project, with the ProjectID. func ResolveProject(ctx context.Context, reader client.Reader, src client.Object, ref *refs.ProjectRef) (*Project, error) { if ref == nil { return nil, nil @@ -82,12 +83,9 @@ func ResolveProject(ctx context.Context, reader client.Reader, src client.Object return nil, fmt.Errorf("error reading referenced Project %v: %w", key, err) } - projectID, _, err := unstructured.NestedString(project.Object, "spec", "resourceID") + projectID, err := GetResourceID(project) if err != nil { - return nil, fmt.Errorf("reading spec.resourceID from Project %v: %w", key, err) - } - if projectID == "" { - projectID = project.GetName() + return nil, err } return &Project{ diff --git a/pkg/controller/direct/references/resourceid.go b/pkg/controller/direct/references/resourceid.go new file mode 100644 index 0000000000..929242f664 --- /dev/null +++ b/pkg/controller/direct/references/resourceid.go @@ -0,0 +1,32 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package references + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func GetResourceID(u *unstructured.Unstructured) (string, error) { + resourceID, _, err := unstructured.NestedString(u.Object, "spec", "resourceID") + if err != nil { + return "", fmt.Errorf("reading spec.resourceID from %v %v/%v: %w", u.GroupVersionKind().Kind, u.GetNamespace(), u.GetName(), err) + } + if resourceID == "" { + resourceID = u.GetName() + } + return resourceID, nil +} diff --git a/pkg/controller/direct/register/register.go b/pkg/controller/direct/register/register.go index f9e73cd4f1..23a9957548 100644 --- a/pkg/controller/direct/register/register.go +++ b/pkg/controller/direct/register/register.go @@ -20,5 +20,6 @@ import ( _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/cloudbuild" _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/gkehub" _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/logging" + _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/monitoring" _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/resourcemanager" ) diff --git a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_generated_object_monitoringdashboardbasic.golden.yaml b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_generated_object_monitoringdashboardbasic.golden.yaml index 138280de99..81e1f8d473 100644 --- a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_generated_object_monitoringdashboardbasic.golden.yaml +++ b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_generated_object_monitoringdashboardbasic.golden.yaml @@ -3,7 +3,6 @@ kind: MonitoringDashboard metadata: annotations: cnrm.cloud.google.com/management-conflict-prevention-policy: none - cnrm.cloud.google.com/state-into-spec: merge finalizers: - cnrm.cloud.google.com/finalizer - cnrm.cloud.google.com/deletion-defender diff --git a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_http.log b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_http.log index caf1a0e4cb..a81a02ff3a 100644 --- a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_http.log +++ b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardbasic/_http.log @@ -1,6 +1,7 @@ -GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2F${projectId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 404 Not Found Cache-Control: private @@ -23,26 +24,27 @@ X-Xss-Protection: 0 --- -POST https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards?alt=json +POST https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: parent=projects%2F${projectId} { "columnLayout": { "columns": [ { - "weight": 2, + "weight": "2", "widgets": [ { "title": "Widget 1", "xyChart": { "dataSets": [ { - "plotType": "LINE", + "plotType": 1, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -53,14 +55,14 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, { "text": { "content": "Widget 2", - "format": "MARKDOWN" + "format": 1 } }, { @@ -68,11 +70,11 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "xyChart": { "dataSets": [ { - "plotType": "STACKED_BAR", + "plotType": 3, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -83,7 +85,7 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, @@ -198,9 +200,10 @@ X-Xss-Protection: 0 --- -GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2F${projectId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 200 OK Cache-Control: private @@ -296,26 +299,27 @@ X-Xss-Protection: 0 --- -PATCH https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +PATCH https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: dashboard.name=projects%2F${projectId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} { "columnLayout": { "columns": [ { - "weight": 2, + "weight": "2", "widgets": [ { "title": "Widget 1", "xyChart": { "dataSets": [ { - "plotType": "LINE", + "plotType": 1, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -323,18 +327,17 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 } } ], - "thresholds": [], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, { "text": { "content": "Widget 2", - "format": "MARKDOWN" + "format": 1 } }, { @@ -342,11 +345,11 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "xyChart": { "dataSets": [ { - "plotType": "STACKED_BAR", + "plotType": 3, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -354,11 +357,10 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 } } ], - "thresholds": [], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, @@ -373,7 +375,6 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 ] }, "displayName": "monitoringdashboard-updated", - "etag": "abcdef0123A=", "name": "projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}" } @@ -468,9 +469,10 @@ X-Xss-Protection: 0 --- -GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2F${projectId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 200 OK Cache-Control: private @@ -563,9 +565,10 @@ X-Xss-Protection: 0 --- -DELETE https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +DELETE https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2F${projectId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 200 OK Cache-Control: private @@ -578,29 +581,4 @@ X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-Xss-Protection: 0 -{} - ---- - -GET https://monitoring.googleapis.com/v1/projects/${projectId}/dashboards/monitoringdashboard-${uniqueId}?alt=json -Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 - -404 Not Found -Cache-Control: private -Content-Type: application/json; charset=UTF-8 -Server: ESF -Vary: Origin -Vary: X-Origin -Vary: Referer -X-Content-Type-Options: nosniff -X-Frame-Options: SAMEORIGIN -X-Xss-Protection: 0 - -{ - "error": { - "code": 404, - "message": "Requested entity was not found.", - "status": "NOT_FOUND" - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_generated_object_monitoringdashboardrefs.golden.yaml b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_generated_object_monitoringdashboardrefs.golden.yaml index 61ef98a721..f3ebe5fcc7 100644 --- a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_generated_object_monitoringdashboardrefs.golden.yaml +++ b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_generated_object_monitoringdashboardrefs.golden.yaml @@ -3,7 +3,6 @@ kind: MonitoringDashboard metadata: annotations: cnrm.cloud.google.com/management-conflict-prevention-policy: none - cnrm.cloud.google.com/state-into-spec: merge finalizers: - cnrm.cloud.google.com/finalizer - cnrm.cloud.google.com/deletion-defender diff --git a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_http.log b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_http.log index ea27a856ec..7d05004891 100644 --- a/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_http.log +++ b/pkg/test/resourcefixture/testdata/basic/monitoring/v1beta1/monitoringdashboard/monitoringdashboardrefs/_http.log @@ -150,9 +150,10 @@ X-Xss-Protection: 0 --- -GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2Fother${uniqueId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 404 Not Found Cache-Control: private @@ -175,26 +176,27 @@ X-Xss-Protection: 0 --- -POST https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards?alt=json +POST https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: parent=projects%2Fother${uniqueId} { "columnLayout": { "columns": [ { - "weight": 2, + "weight": "2", "widgets": [ { "title": "Widget 1", "xyChart": { "dataSets": [ { - "plotType": "LINE", + "plotType": 1, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -205,14 +207,14 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, { "text": { "content": "Widget 2", - "format": "MARKDOWN" + "format": 1 } }, { @@ -220,11 +222,11 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "xyChart": { "dataSets": [ { - "plotType": "STACKED_BAR", + "plotType": 3, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -235,7 +237,7 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, @@ -350,9 +352,10 @@ X-Xss-Protection: 0 --- -GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2Fother${uniqueId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 200 OK Cache-Control: private @@ -448,26 +451,27 @@ X-Xss-Protection: 0 --- -PATCH https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +PATCH https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: dashboard.name=projects%2Fother${uniqueId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} { "columnLayout": { "columns": [ { - "weight": 2, + "weight": "2", "widgets": [ { "title": "Widget 1", "xyChart": { "dataSets": [ { - "plotType": "LINE", + "plotType": 1, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -475,18 +479,17 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 } } ], - "thresholds": [], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, { "text": { "content": "Widget 2", - "format": "MARKDOWN" + "format": 1 } }, { @@ -494,11 +497,11 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 "xyChart": { "dataSets": [ { - "plotType": "STACKED_BAR", + "plotType": 3, "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { - "perSeriesAligner": "ALIGN_RATE" + "perSeriesAligner": 2 }, "filter": "metric.type=\"agent.googleapis.com/nginx/connections/accepted_count\"" }, @@ -506,11 +509,10 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 } } ], - "thresholds": [], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", - "scale": "LINEAR" + "scale": 1 } } }, @@ -525,8 +527,7 @@ User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 ] }, "displayName": "monitoringdashboard updated", - "etag": "abcdef0123A=", - "name": "projects/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}" + "name": "projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}" } 200 OK @@ -620,9 +621,10 @@ X-Xss-Protection: 0 --- -GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2Fother${uniqueId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 200 OK Cache-Control: private @@ -715,9 +717,10 @@ X-Xss-Protection: 0 --- -DELETE https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?alt=json +DELETE https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?%24alt=json%3Benum-encoding%3Dint Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 +User-Agent: kcc/controller-manager +x-goog-request-params: name=projects%2Fother${uniqueId}%2Fdashboards%2Fmonitoringdashboard-${uniqueId} 200 OK Cache-Control: private @@ -734,31 +737,6 @@ X-Xss-Protection: 0 --- -GET https://monitoring.googleapis.com/v1/projects/other${uniqueId}/dashboards/monitoringdashboard-${uniqueId}?alt=json -Content-Type: application/json -User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 - -404 Not Found -Cache-Control: private -Content-Type: application/json; charset=UTF-8 -Server: ESF -Vary: Origin -Vary: X-Origin -Vary: Referer -X-Content-Type-Options: nosniff -X-Frame-Options: SAMEORIGIN -X-Xss-Protection: 0 - -{ - "error": { - "code": 404, - "message": "Requested entity was not found.", - "status": "NOT_FOUND" - } -} - ---- - GET https://cloudresourcemanager.googleapis.com/v1/projects/other${uniqueId}?alt=json&prettyPrint=false User-Agent: google-api-go-client/0.5 Terraform/ (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google-beta/kcc/controller-manager diff --git a/scripts/github-actions/tests-e2e-direct.sh b/scripts/github-actions/tests-e2e-direct.sh index 8db23e4a46..efe45ff93b 100755 --- a/scripts/github-actions/tests-e2e-direct.sh +++ b/scripts/github-actions/tests-e2e-direct.sh @@ -24,24 +24,27 @@ cd ${REPO_ROOT}/ echo "Downloading envtest assets..." export KUBEBUILDER_ASSETS=$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@latest use -p path) -#export KCC_USE_DIRECT_RECONCILERS=LoggingLogMetric +export KCC_USE_DIRECT_RECONCILERS=MonitoringDashboard echo "Running e2e tests samples for LoggingLogMetric direct reconciliation..." - GOLDEN_OBJECT_CHECKS=1 \ GOLDEN_REQUEST_CHECKS=1 \ E2E_KUBE_TARGET=envtest RUN_E2E=1 E2E_GCP_TARGET=mock \ go test -test.count=1 -timeout 600s -v ./tests/e2e -run 'TestAllInSeries/samples/linear-log-metric|TestAllInSeries/samples/exponential-log-metric|TestAllInSeries/samples/int-log-metric|TestAllInSeries/samples/explicit-log-metric' echo "Running e2e tests fixtures for LoggingLogMetric direct reconciliation..." - GOLDEN_OBJECT_CHECKS=1 \ GOLDEN_REQUEST_CHECKS=1 \ E2E_KUBE_TARGET=envtest RUN_E2E=1 E2E_GCP_TARGET=mock \ go test -test.count=1 -timeout 600s -v ./tests/e2e -run 'TestAllInSeries/fixtures/explicitlogmetric|TestAllInSeries/fixtures/exponentiallogmetric|TestAllInSeries/fixtures/linearlogmetric|TestAllInSeries/fixtures/logbucketmetric|TestAllInSeries/fixtures/monitoringdashboard' -echo "Running scenarios tests for LoggingLogMetric direct reconciliation..." +echo "Running e2e tests fixtures for MonitoringDashboard direct reconciliation..." +GOLDEN_OBJECT_CHECKS=1 \ +GOLDEN_REQUEST_CHECKS=1 \ +E2E_KUBE_TARGET=envtest RUN_E2E=1 E2E_GCP_TARGET=mock \ + go test -test.count=1 -timeout 600s -v ./tests/e2e -run 'TestAllInSeries/fixtures/monitoringdashboard' +echo "Running scenarios tests for LoggingLogMetric direct reconciliation..." GOLDEN_REQUEST_CHECKS=1 E2E_KUBE_TARGET=envtest E2E_GCP_TARGET=mock RUN_E2E=1 \ go test -test.count=1 -timeout 360s -v ./tests/e2e -run TestE2EScript/scenarios/fields