Skip to content

Commit

Permalink
Merge pull request GoogleCloudPlatform#1192 from justinsb/mockgcp_tag…
Browse files Browse the repository at this point in the history
…s_tagkey

mockgcp: add mock for TagsTagKey
  • Loading branch information
google-oss-prow[bot] authored Feb 9, 2024
2 parents 8d4ec8a + b751ad3 commit 8a202ed
Show file tree
Hide file tree
Showing 12 changed files with 956 additions and 84 deletions.
2 changes: 2 additions & 0 deletions config/tests/samples/create/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,8 @@ func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured
case schema.GroupKind{Group: "serviceusage.cnrm.cloud.google.com", Kind: "Service"}:
case schema.GroupKind{Group: "serviceusage.cnrm.cloud.google.com", Kind: "ServiceIdentity"}:

case schema.GroupKind{Group: "tags.cnrm.cloud.google.com", Kind: "TagsTagKey"}:

default:
t.Skipf("gk %v not suppported by mock gcp; skipping", gvk.GroupKind())
}
Expand Down
73 changes: 73 additions & 0 deletions mockgcp/common/httpmux/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 httpmux

import (
"strings"

"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"k8s.io/klog/v2"
)

func MarshalAsJSON(obj proto.Message) ([]byte, error) {
return protojson.MarshalOptions{Resolver: &protoResolver{}}.Marshal(obj)
}

type protoResolver struct {
}

var _ protoregistry.ExtensionTypeResolver = &protoResolver{}

func (r *protoResolver) FindExtensionByName(message protoreflect.FullName) (protoreflect.ExtensionType, error) {
return protoregistry.GlobalTypes.FindExtensionByName(r.remapName(message))
}

func (r *protoResolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
return protoregistry.GlobalTypes.FindExtensionByNumber(r.remapName(message), field)
}

var _ protoregistry.MessageTypeResolver = &protoResolver{}

func (r *protoResolver) FindMessageByName(message protoreflect.FullName) (protoreflect.MessageType, error) {
return protoregistry.GlobalTypes.FindMessageByName(r.remapName(message))
}

func (r *protoResolver) FindMessageByURL(url string) (protoreflect.MessageType, error) {
if strings.HasPrefix(url, "type.googleapis.com/google.") {
s := "type.googleapis.com/mockgcp." + strings.TrimPrefix(url, "type.googleapis.com/google.")
mt, err := protoregistry.GlobalTypes.FindMessageByURL(s)
if err != nil {
klog.Warningf("FindMessageByURL(%q) failed: %v", s, err)
} else {
return mt, nil
}
}

return protoregistry.GlobalTypes.FindMessageByURL(url)
}

func (r *protoResolver) remapName(name protoreflect.FullName) protoreflect.FullName {
// Remap names with a prefix of "google."" to be "mockgcp.", so we can find them.

s := string(name)
if strings.HasPrefix(s, "google.") {
s = "mockgcp." + strings.TrimPrefix(s, "google.")
return protoreflect.FullName(s)
}
return name
}
3 changes: 3 additions & 0 deletions mockgcp/common/httpmux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ import (

// NewServeMux constructs an http server with our error handling etc
func NewServeMux(ctx context.Context, conn *grpc.ClientConn, handlers ...func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error) (*runtime.ServeMux, error) {
resolver := &protoResolver{}
marshaler := &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
EmitUnpopulated: false,
Resolver: resolver,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
Resolver: resolver,
},
},
}
Expand Down
75 changes: 75 additions & 0 deletions mockgcp/common/operations/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 operations

import (
"context"
"net/http"

"cloud.google.com/go/longrunning/autogen/longrunningpb"
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/httpmux"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/klog/v2"
)

func (s *Operations) RegisterOperationsHandler(prefix string) func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
forwardResponseOptions := mux.GetForwardResponseOptions()

// GET /{prefix}/operations/{name}
if err := mux.HandlePath("GET", "/"+prefix+"/operations/{name}", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
ctx := r.Context()
name := pathParams["name"]
req := &longrunningpb.GetOperationRequest{Name: "operations/" + name}
op, err := s.GetOperation(ctx, req)
if err != nil {
if status.Code(err) == codes.NotFound {
klog.Infof("operation not found %+v", req)
w.WriteHeader(http.StatusNotFound)
return
}
klog.Warningf("error getting operation %T: %v", err, err)
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

for _, forwardResponseOption := range forwardResponseOptions {
err := forwardResponseOption(ctx, w, op)
if err != nil {
klog.Warningf("error running forwardResponseOption %T: %v", forwardResponseOption, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

b, err := httpmux.MarshalAsJSON(op)
if err != nil {
klog.Warningf("error converting to proto %T: %v", err, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(b)
}); err != nil {
return err
}
return nil
}
}
21 changes: 20 additions & 1 deletion mockgcp/common/operations/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package operations
import (
"context"
"fmt"
"strings"
"time"

pb "google.golang.org/genproto/googleapis/longrunning"
Expand Down Expand Up @@ -61,7 +62,7 @@ func (s *Operations) NewLRO(ctx context.Context) (*pb.Operation, error) {
return op, nil
}

func (s *Operations) StartLRO(ctx context.Context, callback func() (proto.Message, error)) (*pb.Operation, error) {
func (s *Operations) StartLRO(ctx context.Context, metadata proto.Message, callback func() (proto.Message, error)) (*pb.Operation, error) {
now := time.Now()
millis := now.UnixMilli()
id := uuid.NewUUID()
Expand All @@ -71,6 +72,15 @@ func (s *Operations) StartLRO(ctx context.Context, callback func() (proto.Messag
op.Name = fmt.Sprintf("operations/operation-%d-%s", millis, id)
op.Done = false

if metadata != nil {
metadataAny, err := anypb.New(metadata)
if err != nil {
return nil, fmt.Errorf("error building anypb for metadata: %w", err)
}
rewriteTypes(metadataAny)

op.Metadata = metadataAny
}
fqn := op.Name

if err := s.storage.Create(ctx, fqn, op); err != nil {
Expand Down Expand Up @@ -98,6 +108,8 @@ func (s *Operations) StartLRO(ctx context.Context, callback func() (proto.Messag
klog.Warningf("error building anypb for result: %v", err)
finished.Result = &pb.Operation_Response{}
} else {
rewriteTypes(resultAny)

finished.Result = &pb.Operation_Response{
Response: resultAny,
}
Expand All @@ -112,6 +124,13 @@ func (s *Operations) StartLRO(ctx context.Context, callback func() (proto.Messag
return op, nil
}

func rewriteTypes(any *anypb.Any) {
// Fix our mockgcp hack
if strings.HasPrefix(any.TypeUrl, "type.googleapis.com/mockgcp.") {
any.TypeUrl = "type.googleapis.com/google." + strings.TrimPrefix(any.TypeUrl, "type.googleapis.com/mockgcp.")
}
}

// Gets the latest state of a long-running operation. Clients can use this
// method to poll the operation result at intervals as recommended by the API
// service.
Expand Down
68 changes: 8 additions & 60 deletions mockgcp/mockresourcemanager/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,12 @@ package mockresourcemanager

import (
"context"
"encoding/json"
"net/http"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/httpmux"
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/operations"
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
pb_v1 "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/mockgcp/cloud/resourcemanager/v1"
Expand Down Expand Up @@ -69,66 +65,18 @@ func (s *MockService) ExpectedHost() string {
func (s *MockService) Register(grpcServer *grpc.Server) {
pb_v1.RegisterProjectsServer(grpcServer, s.projectsV1)
pb_v3.RegisterProjectsServer(grpcServer, s.projectsV3)
pb_v3.RegisterTagKeysServer(grpcServer, &TagKeys{MockService: s})
}

func (s *MockService) NewHTTPMux(ctx context.Context, conn *grpc.ClientConn) (http.Handler, error) {
mux := runtime.NewServeMux(runtime.WithErrorHandler(customErrorHandler))

if err := pb_v1.RegisterProjectsHandler(ctx, mux, conn); err != nil {
return nil, err
}

if err := pb_v3.RegisterProjectsHandler(ctx, mux, conn); err != nil {
mux, err := httpmux.NewServeMux(ctx, conn,
pb_v1.RegisterProjectsHandler,
pb_v3.RegisterProjectsHandler,
pb_v3.RegisterTagKeysHandler,
s.operations.RegisterOperationsHandler("v3"))
if err != nil {
return nil, err
}

return mux, nil
}

type wrappedStatus struct {
Error *wrappedError `json:"error,omitempty"`
}

type wrappedError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Status string `json:"status,omitempty"`
}

// customErrorHandler wraps errors in an error blockk
func customErrorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
s := status.Convert(err)
// pb := s.Proto()

w.Header().Del("Trailer")
w.Header().Del("Transfer-Encoding")

w.Header().Set("Content-Type", "application/json; charset=UTF-8")

httpStatusCode := runtime.HTTPStatusFromCode(s.Code())
wrapped := &wrappedStatus{
Error: &wrappedError{
Code: httpStatusCode,
Message: s.Message(),
},
}

switch s.Code() {
case codes.PermissionDenied:
wrapped.Error.Status = "PERMISSION_DENIED"
case codes.AlreadyExists:
wrapped.Error.Status = "ALREADY_EXISTS"
}

buf, merr := json.Marshal(wrapped)
if merr != nil {
klog.Warningf("Failed to marshal error message %q: %v", s, merr)
runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
return
}

w.WriteHeader(httpStatusCode)
if _, err := w.Write(buf); err != nil {
klog.Warningf("Failed to write response: %v", err)
}
}
Loading

0 comments on commit 8a202ed

Please sign in to comment.