mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
94 lines
3.7 KiB
Go
94 lines
3.7 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// parseSemVerConstraint returns a semVer Constraint instance or panic if there is a parsing error with the provided constraint.
|
|
func parseSemVerConstraint(constraintStr string) *semver.Constraints {
|
|
semVerConstraint, err := semver.NewConstraint(constraintStr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return semVerConstraint
|
|
}
|
|
|
|
// UserAgentUnaryServerInterceptor returns a UnaryServerInterceptor which enforces a minimum client
|
|
// version in the user agent
|
|
func UserAgentUnaryServerInterceptor(clientName, constraintStr string) grpc.UnaryServerInterceptor {
|
|
semVerConstraint := parseSemVerConstraint(constraintStr)
|
|
return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
|
if err := userAgentEnforcer(ctx, clientName, semVerConstraint); err != nil {
|
|
return nil, err
|
|
}
|
|
return handler(ctx, req)
|
|
}
|
|
}
|
|
|
|
// UserAgentStreamServerInterceptor returns a StreamServerInterceptor which enforces a minimum client
|
|
// version in the user agent
|
|
func UserAgentStreamServerInterceptor(clientName, constraintStr string) grpc.StreamServerInterceptor {
|
|
semVerConstraint := parseSemVerConstraint(constraintStr)
|
|
return func(srv any, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
if err := userAgentEnforcer(stream.Context(), clientName, semVerConstraint); err != nil {
|
|
return err
|
|
}
|
|
return handler(srv, stream)
|
|
}
|
|
}
|
|
|
|
func userAgentEnforcer(ctx context.Context, clientName string, semVerConstraint *semver.Constraints) error {
|
|
var userAgents []string
|
|
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
|
for _, ua := range md["user-agent"] {
|
|
// ua is a string like "argocd-client/v0.11.0+cde040e grpc-go/1.15.0"
|
|
userAgents = append(userAgents, strings.Fields(ua)...)
|
|
break
|
|
}
|
|
}
|
|
if isLegacyClient(userAgents) {
|
|
return status.Errorf(codes.FailedPrecondition, "unsatisfied client version constraint: %s", semVerConstraint)
|
|
}
|
|
|
|
for _, userAgent := range userAgents {
|
|
uaSplit := strings.Split(userAgent, "/")
|
|
if len(uaSplit) != 2 || uaSplit[0] != clientName {
|
|
// User-agent was supplied, but client/format is not one we care about (e.g. grpc-go)
|
|
continue
|
|
}
|
|
// remove pre-release part
|
|
versionStr := strings.Split(uaSplit[1], "-")[0]
|
|
// We have matched the client name to the one we care about
|
|
uaVers, err := semver.NewVersion(versionStr)
|
|
if err != nil {
|
|
return status.Errorf(codes.InvalidArgument, "could not parse version from user-agent: %s", userAgent)
|
|
}
|
|
if ok, errs := semVerConstraint.Validate(uaVers); !ok {
|
|
return status.Errorf(codes.FailedPrecondition, "unsatisfied client version constraint: %s", errors.Join(errs...))
|
|
}
|
|
return nil
|
|
}
|
|
// If we get here, the caller either did not supply user-agent, supplied one which we don't
|
|
// care about. This implies it is a from a custom generated client, so we permit the request.
|
|
// We really only want to enforce user-agent version constraints for clients under our
|
|
// control which we know to have compatibility issues
|
|
return nil
|
|
}
|
|
|
|
// isLegacyClient checks if the request was made from a legacy Argo CD client (i.e. v0.10 CLI).
|
|
// The heuristic is that a single default 'grpc-go' user-agent was specified with one of the
|
|
// previous versions of grpc-go we used in the past (1.15.0, 1.10.0).
|
|
// Starting in v0.11, both of the gRPC clients we maintain (pkg/apiclient and grpc-gateway) started
|
|
// supplying a explicit user-agent tied to the Argo CD version.
|
|
func isLegacyClient(userAgents []string) bool {
|
|
return len(userAgents) == 1 && (userAgents[0] == "grpc-go/1.15.0" || userAgents[0] == "grpc-go/1.10.0")
|
|
}
|