-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathchaos_service.go
134 lines (105 loc) · 4.77 KB
/
chaos_service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.
package grpc
import (
"context"
"fmt"
"math/rand"
"sync"
v1beta1 "github.com/DataDog/chaos-controller/api/v1beta1"
grpccalc "github.com/DataDog/chaos-controller/grpc/calculations"
pb "github.com/DataDog/chaos-controller/grpc/disruptionlistener"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// ChaosDisruptionListener is a gRPC Service that can disrupt endpoints of a gRPC server.
// The interface it is implementing was generated in the grpc/disruptionlistener package.
type ChaosDisruptionListener struct {
pb.UnimplementedDisruptionListenerServer
configuration grpccalc.DisruptionConfiguration
mutex sync.Mutex
logger *zap.SugaredLogger
}
// NewDisruptionListener creates a new DisruptionListener Service with the logger instantiated and DisruptionConfiguration set to be empty
func NewDisruptionListener(logger *zap.SugaredLogger) *ChaosDisruptionListener {
d := ChaosDisruptionListener{}
d.logger = logger
d.configuration = grpccalc.DisruptionConfiguration{}
return &d
}
// Disrupt receives a disruption specification and configures the interceptor to spoof responses to specified endpoints.
func (d *ChaosDisruptionListener) Disrupt(ctx context.Context, ds *pb.DisruptionSpec) (*emptypb.Empty, error) {
if ds == nil {
d.logger.Error("cannot execute Disrupt when DisruptionSpec is nil")
return nil, status.Error(codes.InvalidArgument, "Cannot execute Disrupt when DisruptionSpec is nil")
}
config := grpccalc.DisruptionConfiguration{}
for _, endpointSpec := range ds.Endpoints {
if endpointSpec.TargetEndpoint == "" {
d.logger.Error("DisruptionSpec does not specify TargetEndpoint for at least one endpointAlteration")
return nil, status.Error(codes.InvalidArgument, "Cannot execute Disrupt without specifying TargetEndpoint for all endpointAlterations")
}
Alterations, err := grpccalc.ConvertSpecifications(endpointSpec.Alterations)
if err != nil {
return nil, err
}
// add endpoint to main configuration
targetEndpoint := grpccalc.TargetEndpoint(endpointSpec.TargetEndpoint)
config[targetEndpoint] = grpccalc.EndpointConfiguration{
TargetEndpoint: targetEndpoint,
Alterations: Alterations,
}
}
if len(d.configuration) > 0 {
d.logger.Error("cannot apply new DisruptionSpec when DisruptionListener is already configured")
return nil, status.Error(codes.AlreadyExists, "Cannot apply new DisruptionSpec when DisruptionListener is already configured")
}
d.mutex.Lock()
select {
case <-ctx.Done():
d.logger.Error("cannot apply new DisruptionSpec, gRPC request was canceled")
default:
d.configuration = config
}
d.mutex.Unlock()
return &emptypb.Empty{}, nil
}
// ResetDisruptions removes all configured endpoint alterations for DisruptionListener.
func (d *ChaosDisruptionListener) ResetDisruptions(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
d.mutex.Lock()
d.configuration = map[grpccalc.TargetEndpoint]grpccalc.EndpointConfiguration{}
d.mutex.Unlock()
return &emptypb.Empty{}, nil
}
// ChaosServerInterceptor is a function which can be registered on instantiation of a gRPC server
// to intercept all traffic to the server and crosscheck their endpoints to disrupt them.
func (d *ChaosDisruptionListener) ChaosServerInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response interface{}, err error) {
d.logger.Debug("comparing with %s with %d endpoints", info.FullMethod, len(d.configuration))
// FullMethod is the full RPC method string, i.e., /package.service/method.
targetEndpoint := grpccalc.TargetEndpoint(info.FullMethod)
if endptConfig, ok := d.configuration[targetEndpoint]; ok {
randomPercent := rand.Intn(100)
if len(endptConfig.Alterations) > randomPercent {
altConfig := endptConfig.Alterations[randomPercent]
if altConfig.ErrorToReturn != "" {
d.logger.Debug("error code to return: %s", v1beta1.ErrorMap[altConfig.ErrorToReturn])
return nil, status.Error(
v1beta1.ErrorMap[altConfig.ErrorToReturn],
// Future Work: interview users about this message //nolint:golint
fmt.Sprintf("Chaos Controller injected this error: %s", altConfig.ErrorToReturn),
)
} else if altConfig.OverrideToReturn != "" {
d.logger.Debug("override to return: %s", altConfig.OverrideToReturn)
return &emptypb.Empty{}, nil
}
d.logger.Error("endpoint %s should define either an ErrorToReturn or OverrideToReturn but does not", endptConfig.TargetEndpoint)
}
}
return handler(ctx, req)
}