go.temporal.io/server@v1.23.0/common/headers/version_checker.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package headers
    26  
    27  import (
    28  	"context"
    29  	"fmt"
    30  	"strings"
    31  
    32  	"github.com/blang/semver/v4"
    33  	"golang.org/x/exp/slices"
    34  	"google.golang.org/grpc/metadata"
    35  
    36  	"go.temporal.io/api/serviceerror"
    37  )
    38  
    39  const (
    40  	ClientNameServer        = "temporal-server"
    41  	ClientNameServerHTTP    = "temporal-server-http"
    42  	ClientNameGoSDK         = "temporal-go"
    43  	ClientNameJavaSDK       = "temporal-java"
    44  	ClientNamePHPSDK        = "temporal-php"
    45  	ClientNameTypeScriptSDK = "temporal-typescript"
    46  	ClientNamePythonSDK     = "temporal-python"
    47  	ClientNameCLI           = "temporal-cli"
    48  	ClientNameUI            = "temporal-ui"
    49  
    50  	// ServerVersion value can be changed by the create-tag Github workflow.
    51  	// If you change the var name or move it, be sure to update the workflow.
    52  	ServerVersion = "1.23.0"
    53  
    54  	// SupportedServerVersions is used by CLI and inter role communication.
    55  	SupportedServerVersions = ">=1.0.0 <2.0.0"
    56  
    57  	// FeatureFollowsNextRunID means that the client supports following next execution run id for
    58  	// completed/failed/timedout completion events when getting the final result of a workflow.
    59  	FeatureFollowsNextRunID = "follows-next-run-id"
    60  )
    61  
    62  var (
    63  	// AllFeatures contains all known features. This list is used as the value of the supported
    64  	// features header for internal server requests. There is an assumption that if a feature is
    65  	// defined, then the server itself supports it.
    66  	AllFeatures = strings.Join([]string{
    67  		FeatureFollowsNextRunID,
    68  	}, SupportedFeaturesHeaderDelim)
    69  
    70  	SupportedClients = map[string]string{
    71  		ClientNameGoSDK:         "<2.0.0",
    72  		ClientNameJavaSDK:       "<2.0.0",
    73  		ClientNamePHPSDK:        "<2.0.0",
    74  		ClientNameTypeScriptSDK: "<2.0.0",
    75  		ClientNameCLI:           "<2.0.0",
    76  		ClientNameServer:        "<2.0.0",
    77  		ClientNameUI:            "<3.0.0",
    78  	}
    79  
    80  	internalVersionHeaderPairs = []string{
    81  		ClientNameHeaderName, ClientNameServer,
    82  		ClientVersionHeaderName, ServerVersion,
    83  		SupportedServerVersionsHeaderName, SupportedServerVersions,
    84  		SupportedFeaturesHeaderName, AllFeatures,
    85  	}
    86  )
    87  
    88  type (
    89  	// VersionChecker is used to check client/server compatibility and client's capabilities
    90  	VersionChecker interface {
    91  		ClientSupported(ctx context.Context) error
    92  		ClientSupportsFeature(ctx context.Context, feature string) bool
    93  	}
    94  
    95  	versionChecker struct {
    96  		supportedClients      map[string]string
    97  		supportedClientsRange map[string]semver.Range
    98  		serverVersion         semver.Version
    99  	}
   100  )
   101  
   102  // NewDefaultVersionChecker constructs a new VersionChecker using default versions from const.
   103  func NewDefaultVersionChecker() *versionChecker {
   104  	return NewVersionChecker(SupportedClients, ServerVersion)
   105  }
   106  
   107  // NewVersionChecker constructs a new VersionChecker
   108  func NewVersionChecker(supportedClients map[string]string, serverVersion string) *versionChecker {
   109  	return &versionChecker{
   110  		serverVersion:         semver.MustParse(serverVersion),
   111  		supportedClients:      supportedClients,
   112  		supportedClientsRange: mustParseRanges(supportedClients),
   113  	}
   114  }
   115  
   116  // GetClientNameAndVersion extracts SDK name and version from context headers
   117  func GetClientNameAndVersion(ctx context.Context) (string, string) {
   118  	headers := GetValues(ctx, ClientNameHeaderName, ClientVersionHeaderName)
   119  	clientName := headers[0]
   120  	clientVersion := headers[1]
   121  	return clientName, clientVersion
   122  }
   123  
   124  // SetVersions sets headers for internal communications.
   125  func SetVersions(ctx context.Context) context.Context {
   126  	return metadata.AppendToOutgoingContext(ctx, internalVersionHeaderPairs...)
   127  }
   128  
   129  // SetVersionsForTests sets headers as they would be received from the client.
   130  // Must be used in tests only.
   131  func SetVersionsForTests(ctx context.Context, clientVersion, clientName, supportedServerVersions, supportedFeatures string) context.Context {
   132  	return metadata.NewIncomingContext(ctx, metadata.New(map[string]string{
   133  		ClientNameHeaderName:              clientName,
   134  		ClientVersionHeaderName:           clientVersion,
   135  		SupportedServerVersionsHeaderName: supportedServerVersions,
   136  		SupportedFeaturesHeaderName:       supportedFeatures,
   137  	}))
   138  }
   139  
   140  // ClientSupported returns an error if client is unsupported, nil otherwise.
   141  func (vc *versionChecker) ClientSupported(ctx context.Context) error {
   142  
   143  	headers := GetValues(ctx, ClientNameHeaderName, ClientVersionHeaderName, SupportedServerVersionsHeaderName)
   144  	clientName := headers[0]
   145  	clientVersion := headers[1]
   146  	supportedServerVersions := headers[2]
   147  
   148  	// Validate client version only if it is provided and server knows about this client.
   149  	if clientName != "" && clientVersion != "" {
   150  		if supportedClientRange, ok := vc.supportedClientsRange[clientName]; ok {
   151  			clientVersionParsed, parseErr := semver.Parse(clientVersion)
   152  			if parseErr != nil {
   153  				return serviceerror.NewInvalidArgument(fmt.Sprintf("Unable to parse client version: %v", parseErr))
   154  			}
   155  			if !supportedClientRange(clientVersionParsed) {
   156  				return serviceerror.NewClientVersionNotSupported(clientVersion, clientName, vc.supportedClients[clientName])
   157  			}
   158  		}
   159  	}
   160  
   161  	// Validate supported server version if it is provided.
   162  	if supportedServerVersions != "" {
   163  		supportedServerVersionsParsed, parseErr := semver.ParseRange(supportedServerVersions)
   164  		if parseErr != nil {
   165  			return serviceerror.NewInvalidArgument(fmt.Sprintf("Unable to parse supported server versions: %v", parseErr))
   166  		}
   167  		if !supportedServerVersionsParsed(vc.serverVersion) {
   168  			return serviceerror.NewServerVersionNotSupported(vc.serverVersion.String(), supportedServerVersions)
   169  		}
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  // ClientSupportsFeature returns true if the client reports support for the
   176  // given feature (which should be one of the Feature... constants above).
   177  func (vc *versionChecker) ClientSupportsFeature(ctx context.Context, feature string) bool {
   178  	headers := GetValues(ctx, SupportedFeaturesHeaderName)
   179  	clientFeatures := strings.Split(headers[0], SupportedFeaturesHeaderDelim)
   180  	return slices.Contains(clientFeatures, feature)
   181  }
   182  
   183  func mustParseRanges(ranges map[string]string) map[string]semver.Range {
   184  	out := make(map[string]semver.Range, len(ranges))
   185  	for c, r := range ranges {
   186  		out[c] = semver.MustParseRange(r)
   187  	}
   188  	return out
   189  }