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 }