Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin: implement admin services #4274

Merged
merged 11 commits into from
Mar 22, 2021
57 changes: 57 additions & 0 deletions admin/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
*
* Copyright 2021 gRPC authors.
*
* 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 admin provides a convenient method for registering a collection of
// administration services to a gRPC server. The services registered are:
//
// - Channelz: https://github.com/grpc/proposal/blob/master/A14-channelz.md
// - CSDS: https://github.com/grpc/proposal/blob/master/A40-csds-support.md
//
// Experimental
//
// Notice: All APIs in this package are experimental and may be removed in a
// later release.
package admin

import (
"google.golang.org/grpc"
channelzservice "google.golang.org/grpc/channelz/service"
internaladmin "google.golang.org/grpc/internal/admin"
)

func init() {
// Add a list of default services to admin here. Optional services, like
// CSDS, will be added by other packages.
internaladmin.AddService(func(registrar grpc.ServiceRegistrar) (func(), error) {
channelzservice.RegisterChannelzServiceToServer(registrar)
return nil, nil
})
}

// Register registers the set of admin services to the given server.
//
// The returned cleanup function should be called to clean up the resources
// allocated for the service handlers after the server is stopped.
//
// Note that if `s` is not a *grpc.Server or a *xds.GRPCServer, CSDS will not be
// registered because CSDS generated code is old and doesn't support interface
// `grpc.ServiceRegistrar`.
// https://github.com/envoyproxy/go-control-plane/issues/403
func Register(s grpc.ServiceRegistrar) (cleanup func(), _ error) {
return internaladmin.Register(s)
}
34 changes: 34 additions & 0 deletions admin/admin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
*
* Copyright 2021 gRPC authors.
*
* 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 admin_test

import (
"testing"

"google.golang.org/grpc/admin/test"
"google.golang.org/grpc/codes"
)

func TestRegisterNoCSDS(t *testing.T) {
test.RunRegisterTests(t, test.ExpectedStatusCodes{
ChannelzCode: codes.OK,
// CSDS is not registered because xDS isn't imported.
CSDSCode: codes.Unimplemented,
})
}
38 changes: 38 additions & 0 deletions admin/test/admin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
*
* Copyright 2021 gRPC authors.
*
* 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.
*
*/

// This file has the same content as admin_test.go, difference is that this is
// in another package, and it imports "xds", so we can test that csds is
// registered when xds is imported.

package test_test

import (
"testing"

"google.golang.org/grpc/admin/test"
"google.golang.org/grpc/codes"
_ "google.golang.org/grpc/xds"
)

func TestRegisterWithCSDS(t *testing.T) {
test.RunRegisterTests(t, test.ExpectedStatusCodes{
ChannelzCode: codes.OK,
CSDSCode: codes.OK,
})
}
113 changes: 113 additions & 0 deletions admin/test/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
*
* Copyright 2021 gRPC authors.
*
* 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 test contains test only functions for package admin. It's used by
// admin/admin_test.go and admin/test/admin_test.go.
package test

import (
"context"
"net"
"testing"
"time"

v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
"github.com/google/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/admin"
channelzpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/xds"
"google.golang.org/grpc/status"
)

const (
defaultTestTimeout = 10 * time.Second
)

// ExpectedStatusCodes contains the expected status code for each RPC (can be
// OK).
type ExpectedStatusCodes struct {
ChannelzCode codes.Code
CSDSCode codes.Code
}

// RunRegisterTests makes a client, runs the RPCs, and compares the status
// codes.
func RunRegisterTests(t *testing.T, ec ExpectedStatusCodes) {
nodeID := uuid.New().String()
bootstrapCleanup, err := xds.SetupBootstrapFile(xds.BootstrapOptions{
Version: xds.TransportV3,
NodeID: nodeID,
ServerURI: "no.need.for.a.server",
})
if err != nil {
t.Fatal(err)
}
defer bootstrapCleanup()

lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("cannot create listener: %v", err)
}

server := grpc.NewServer()
defer server.Stop()
cleanup, err := admin.Register(server)
if err != nil {
t.Fatalf("failed to register admin: %v", err)
}
defer cleanup()
go func() {
server.Serve(lis)
}()

conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
if err != nil {
t.Fatalf("cannot connect to server: %v", err)
}

t.Run("channelz", func(t *testing.T) {
if err := RunChannelz(conn); status.Code(err) != ec.ChannelzCode {
t.Fatalf("%s RPC failed with error %v, want code %v", "channelz", err, ec.ChannelzCode)
}
})
t.Run("csds", func(t *testing.T) {
if err := RunCSDS(conn); status.Code(err) != ec.CSDSCode {
t.Fatalf("%s RPC failed with error %v, want code %v", "CSDS", err, ec.CSDSCode)
}
})
}

// RunChannelz makes a channelz RPC.
func RunChannelz(conn *grpc.ClientConn) error {
c := channelzpb.NewChannelzClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, err := c.GetTopChannels(ctx, &channelzpb.GetTopChannelsRequest{}, grpc.WaitForReady(true))
return err
}

// RunCSDS makes a CSDS RPC.
func RunCSDS(conn *grpc.ClientConn) error {
c := v3statuspb.NewClientStatusDiscoveryServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, err := c.FetchClientStatus(ctx, &v3statuspb.ClientStatusRequest{}, grpc.WaitForReady(true))
return err
}
2 changes: 1 addition & 1 deletion channelz/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func init() {
var logger = grpclog.Component("channelz")

// RegisterChannelzServiceToServer registers the channelz service to the given server.
func RegisterChannelzServiceToServer(s *grpc.Server) {
func RegisterChannelzServiceToServer(s grpc.ServiceRegistrar) {
channelzgrpc.RegisterChannelzServer(s, newCZServer())
}

Expand Down
60 changes: 60 additions & 0 deletions internal/admin/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
*
* Copyright 2021 gRPC authors.
*
* 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 admin contains internal implementation for admin service.
package admin

import "google.golang.org/grpc"

// services is a map from name to service register functions.
var services []func(grpc.ServiceRegistrar) (func(), error)

// AddService adds a service to the list of admin services.
//
// NOTE: this function must only be called during initialization time (i.e. in
// an init() function), and is not thread-safe.
//
// If multiple services with the same service name are added (e.g. two services
// for `grpc.channelz.v1.Channelz`), the server will panic on `Register()`.
func AddService(f func(grpc.ServiceRegistrar) (func(), error)) {
services = append(services, f)
}

// Register registers the set of admin services to the given server.
func Register(s grpc.ServiceRegistrar) (cleanup func(), _ error) {
var cleanups []func()
for _, f := range services {
cleanup, err := f(s)
if err != nil {
callFuncs(cleanups)
return nil, err
}
if cleanup != nil {
cleanups = append(cleanups, cleanup)
}
}
return func() {
callFuncs(cleanups)
}, nil
}

func callFuncs(fs []func()) {
for _, f := range fs {
f()
}
}