Skip to content

Commit

Permalink
mockgcp: improve iam serviceaccount to match golden tests
Browse files Browse the repository at this point in the history
  • Loading branch information
justinsb committed Feb 5, 2024
1 parent caffd60 commit c265619
Show file tree
Hide file tree
Showing 6 changed files with 471 additions and 9 deletions.
93 changes: 93 additions & 0 deletions mockgcp/common/httpmux/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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 (
"context"
"encoding/json"
"net/http"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/klog/v2"
)

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"`
Errors []errorDetails `json:"errors,omitempty"`
}

type errorDetails struct {
Domain string `json:"domain,omitempty"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,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"
case codes.NotFound:
wrapped.Error.Status = "NOT_FOUND"
wrapped.Error.Errors = append(wrapped.Error.Errors, errorDetails{
Domain: "global",
Message: wrapped.Error.Message,
Reason: "notFound",
})
}

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
}

if err := addGCPHeaders(ctx, w, nil); err != nil {
klog.Warningf("unexpected error from header filter: %v", err)
}

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

}
79 changes: 79 additions & 0 deletions mockgcp/common/httpmux/mux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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 (
"context"
"net/http"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"k8s.io/klog/v2"
)

// 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) {
marshaler := &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
EmitUnpopulated: false,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
},
}

outgoingHeaderMatcher := func(key string) (string, bool) {
switch key {
case "content-type":
return "", false
default:
klog.Warningf("unknown grpc metadata header %q", key)
return "", false
}
}

mux := runtime.NewServeMux(
runtime.WithErrorHandler(customErrorHandler),
runtime.WithMarshalerOption(runtime.MIMEWildcard, marshaler),
runtime.WithOutgoingHeaderMatcher(outgoingHeaderMatcher),
runtime.WithForwardResponseOption(addGCPHeaders),
)

for _, handler := range handlers {
if err := handler(ctx, mux, conn); err != nil {
return nil, err
}
}

return mux, nil
}

func addGCPHeaders(ctx context.Context, w http.ResponseWriter, resp proto.Message) error {
if w.Header().Get("Content-Type") == "application/json" {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
}
w.Header().Set("Cache-Control", "private")
w.Header().Set("Server", "ESF")
w.Header()["Vary"] = []string{"Origin", "X-Origin", "Referer"}
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("X-Xss-Protection", "0")

return nil
}
4 changes: 4 additions & 0 deletions mockgcp/mock_http_roundtrip.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
response := &http.Response{}
response.Body = ioutil.NopCloser(&body)
response.Header = w.header
if w.statusCode == 0 {
w.statusCode = 200
}
response.Status = fmt.Sprintf("%d %s", w.statusCode, http.StatusText(w.statusCode))
response.StatusCode = w.statusCode
return response, nil
}
Expand Down
9 changes: 2 additions & 7 deletions mockgcp/mockiam/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common"
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/httpmux"
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
pb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/mockgcp/iam/admin/v1"
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/pkg/storage"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)

// MockService represents a mocked IAM service.
Expand Down Expand Up @@ -63,10 +63,5 @@ func (s *MockService) Register(grpcServer *grpc.Server) {
}

func (s *MockService) NewHTTPMux(ctx context.Context, conn *grpc.ClientConn) (http.Handler, error) {
mux := runtime.NewServeMux()
if err := pb.RegisterIAMHandler(ctx, mux, conn); err != nil {
return nil, err
}

return mux, nil
return httpmux.NewServeMux(ctx, conn, pb.RegisterIAMHandler)
}
19 changes: 17 additions & 2 deletions mockgcp/mockiam/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package mockiam

import (
"context"
"crypto/md5"
"regexp"
"strconv"
"time"
Expand All @@ -25,6 +26,7 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/klog/v2"

"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
pb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/mockgcp/iam/admin/v1"
Expand Down Expand Up @@ -59,7 +61,7 @@ func (s *ServerV1) GetServiceAccount(ctx context.Context, req *pb.GetServiceAcco
}

if found == nil {
return nil, status.Errorf(codes.NotFound, "serviceaccount %q not found", req.Name)
return nil, status.Errorf(codes.NotFound, "Service account %q not found", req.Name)
}

return found, nil
Expand All @@ -69,7 +71,7 @@ func (s *ServerV1) GetServiceAccount(ctx context.Context, req *pb.GetServiceAcco
fqn := name.String()
if err := s.storage.Get(ctx, fqn, sa); err != nil {
if apierrors.IsNotFound(err) {
return nil, status.Errorf(codes.NotFound, "serviceaccount %q not found", req.Name)
return nil, status.Errorf(codes.NotFound, "Service account %q not found", req.Name)
}
return nil, status.Errorf(codes.Internal, "error reading serviceaccount: %v", err)
}
Expand Down Expand Up @@ -119,6 +121,9 @@ func (s *ServerV1) CreateServiceAccount(ctx context.Context, req *pb.CreateServi
sa.UniqueId = strconv.FormatInt(uniqueID, 10)
sa.Email = name.Email
sa.DisplayName = displayName
sa.Oauth2ClientId = sa.UniqueId

sa.Etag = computeEtag(sa)

fqn := name.String()
if err := s.storage.Create(ctx, fqn, sa); err != nil {
Expand Down Expand Up @@ -179,3 +184,13 @@ func (s *ServerV1) PatchServiceAccount(ctx context.Context, req *pb.PatchService
}
return sa, nil
}

func computeEtag(obj proto.Message) []byte {
// TODO: Do we risk exposing internal fields? Doesn't matter on a mock, I guess
b, err := proto.Marshal(obj)
if err != nil {
klog.Fatalf("failed to marshal proto object: %v", err)
}
hash := md5.Sum(b)
return hash[:]
}
Loading

0 comments on commit c265619

Please sign in to comment.