github.com/replicatedhq/ship@v0.55.0/pkg/specs/replicatedapp/graphql.go (about) 1 package replicatedapp 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 "strings" 12 "time" 13 14 multierror "github.com/hashicorp/go-multierror" 15 "github.com/pkg/errors" 16 "github.com/replicatedhq/ship/pkg/api" 17 "github.com/replicatedhq/ship/pkg/state" 18 "github.com/spf13/viper" 19 ) 20 21 const ShipRelease = ` 22 id 23 sequence 24 channelId 25 channelName 26 channelIcon 27 semver 28 releaseNotes 29 spec 30 configSpec 31 images { 32 url 33 source 34 appSlug 35 imageKey 36 } 37 githubContents { 38 repo 39 path 40 ref 41 files { 42 name 43 path 44 sha 45 size 46 data 47 } 48 } 49 entitlementSpec 50 entitlements { 51 values { 52 key 53 value 54 labels { 55 key 56 value 57 } 58 } 59 utilizations { 60 key 61 value 62 } 63 meta { 64 lastUpdated 65 customerID 66 installationID 67 } 68 serialized 69 signature 70 } 71 created 72 registrySecret 73 collectSpec 74 analyzeSpec` 75 76 const GetAppspecQuery = ` 77 query($semver: String) { 78 shipRelease (semver: $semver) { 79 ` + ShipRelease + ` 80 } 81 }` 82 83 const GetSlugAppSpecQuery = ` 84 query($appSlug: String!, $licenseID: String, $releaseID: String, $semver: String) { 85 shipSlugRelease (appSlug: $appSlug, licenseID: $licenseID, releaseID: $releaseID, semver: $semver) { 86 ` + ShipRelease + ` 87 } 88 }` 89 90 const GetLicenseQuery = ` 91 query($licenseId: String) { 92 license (licenseId: $licenseId) { 93 id 94 assignee 95 createdAt 96 expiresAt 97 type 98 } 99 }` 100 101 const RegisterInstallQuery = ` 102 mutation($channelId: String!, $releaseId: String!) { 103 shipRegisterInstall( 104 channelId: $channelId 105 releaseId: $releaseId 106 ) 107 }` 108 109 // GraphQLClient is a client for the graphql Payload API 110 type GraphQLClient struct { 111 GQLServer *url.URL 112 Client *http.Client 113 } 114 115 // GraphQLRequest is a json-serializable request to the graphql server 116 type GraphQLRequest struct { 117 Query string `json:"query"` 118 Variables map[string]string `json:"variables"` 119 OperationName string `json:"operationName"` 120 } 121 122 // GraphQLError represents an error returned by the graphql server 123 type GraphQLError struct { 124 Locations []map[string]interface{} `json:"locations"` 125 Message string `json:"message"` 126 Code string `json:"code"` 127 } 128 129 // GQLLicenseResponse is the top-level response object from the graphql server 130 type GQLGetLicenseResponse struct { 131 Data LicenseWrapper `json:"data,omitempty"` 132 Errors []GraphQLError `json:"errors,omitempty"` 133 } 134 135 // GQLGetReleaseResponse is the top-level response object from the graphql server 136 type GQLGetReleaseResponse struct { 137 Data ShipReleaseWrapper `json:"data,omitempty"` 138 Errors []GraphQLError `json:"errors,omitempty"` 139 } 140 141 // GQLGetSlugReleaseResponse is the top-level response object from the graphql server 142 type GQLGetSlugReleaseResponse struct { 143 Data ShipSlugReleaseWrapper `json:"data,omitempty"` 144 Errors []GraphQLError `json:"errors,omitempty"` 145 } 146 147 // ShipReleaseWrapper wraps the release response form GQL 148 type LicenseWrapper struct { 149 License license `json:"license"` 150 } 151 152 // ShipReleaseWrapper wraps the release response form GQL 153 type ShipReleaseWrapper struct { 154 ShipRelease state.ShipRelease `json:"shipRelease"` 155 } 156 157 // ShipSlugReleaseWrapper wraps the release response form GQL 158 type ShipSlugReleaseWrapper struct { 159 ShipSlugRelease state.ShipRelease `json:"shipSlugRelease"` 160 } 161 162 // GQLRegisterInstallResponse is the top-level response object from the graphql server 163 type GQLRegisterInstallResponse struct { 164 Data struct { 165 ShipRegisterInstall bool `json:"shipRegisterInstall"` 166 } `json:"data,omitempty"` 167 Errors []GraphQLError `json:"errors,omitempty"` 168 } 169 170 func parseServerTS(ts string) time.Time { 171 parsed, err := time.Parse(time.RFC3339, ts) 172 if err == nil { 173 return parsed 174 } 175 176 ts = strings.TrimSuffix(ts, "+0000 (UTC)") 177 parsed, err = time.Parse("Mon Jan 02 2006 15:04:05 MST", ts) 178 if err == nil { 179 return parsed 180 } 181 182 ts = strings.TrimSuffix(ts, "+0000 (Coordinated Universal Time)") 183 parsed, err = time.Parse("Mon Jan 02 2006 15:04:05 MST", ts) 184 if err == nil { 185 return parsed 186 } 187 188 return time.Time{} 189 } 190 191 type license struct { 192 ID string `json:"id"` 193 Assignee string `json:"assignee"` 194 CreatedAt string `json:"createdAt"` 195 ExpiresAt string `json:"expiresAt"` 196 Type string `json:"type"` 197 } 198 199 func (l *license) ToLicenseMeta() api.License { 200 return api.License{ 201 ID: l.ID, 202 Assignee: l.Assignee, 203 CreatedAt: parseServerTS(l.CreatedAt), 204 ExpiresAt: parseServerTS(l.ExpiresAt), 205 Type: l.Type, 206 } 207 } 208 209 func (l *license) ToStateLicense() *state.License { 210 return &state.License{ 211 ID: l.ID, 212 Assignee: l.Assignee, 213 CreatedAt: parseServerTS(l.CreatedAt), 214 ExpiresAt: parseServerTS(l.ExpiresAt), 215 Type: l.Type, 216 } 217 } 218 219 type callInfo struct { 220 username string 221 password string 222 request GraphQLRequest 223 upstream string 224 } 225 226 // NewGraphqlClient builds a new client using a viper instance 227 func NewGraphqlClient(v *viper.Viper, client *http.Client) (*GraphQLClient, error) { 228 addr := v.GetString("customer-endpoint") 229 server, err := url.ParseRequestURI(addr) 230 if err != nil { 231 return nil, errors.Wrapf(err, "parse GQL server address %s", addr) 232 } 233 return &GraphQLClient{ 234 GQLServer: server, 235 Client: client, 236 }, nil 237 } 238 239 // GetRelease gets a payload from the graphql server 240 func (c *GraphQLClient) GetRelease(selector *Selector) (*state.ShipRelease, error) { 241 requestObj := GraphQLRequest{ 242 Query: GetAppspecQuery, 243 Variables: map[string]string{ 244 "semver": selector.ReleaseSemver, 245 }, 246 } 247 248 ci := callInfo{ 249 username: selector.GetBasicAuthUsername(), 250 password: selector.InstallationID, 251 request: requestObj, 252 upstream: selector.Upstream, 253 } 254 255 shipResponse := &GQLGetReleaseResponse{} 256 if err := c.callGQL(ci, shipResponse); err != nil { 257 return nil, err 258 } 259 260 if shipResponse.Errors != nil && len(shipResponse.Errors) > 0 { 261 var multiErr *multierror.Error 262 for _, err := range shipResponse.Errors { 263 multiErr = multierror.Append(multiErr, fmt.Errorf("%s: %s", err.Code, err.Message)) 264 265 } 266 return nil, multiErr.ErrorOrNil() 267 } 268 269 return &shipResponse.Data.ShipRelease, nil 270 } 271 272 // GetSlugRelease gets a release from the graphql server by app slug 273 func (c *GraphQLClient) GetSlugRelease(selector *Selector) (*state.ShipRelease, error) { 274 requestObj := GraphQLRequest{ 275 Query: GetSlugAppSpecQuery, 276 Variables: map[string]string{ 277 "appSlug": selector.AppSlug, 278 "licenseID": selector.LicenseID, 279 "releaseID": selector.ReleaseID, 280 "semver": selector.ReleaseSemver, 281 }, 282 } 283 284 ci := callInfo{ 285 username: selector.GetBasicAuthUsername(), 286 password: selector.InstallationID, 287 request: requestObj, 288 upstream: selector.Upstream, 289 } 290 291 shipResponse := &GQLGetSlugReleaseResponse{} 292 if err := c.callGQL(ci, shipResponse); err != nil { 293 return nil, err 294 } 295 296 if shipResponse.Errors != nil && len(shipResponse.Errors) > 0 { 297 var multiErr *multierror.Error 298 for _, err := range shipResponse.Errors { 299 multiErr = multierror.Append(multiErr, fmt.Errorf("%s: %s", err.Code, err.Message)) 300 301 } 302 return nil, multiErr.ErrorOrNil() 303 } 304 305 return &shipResponse.Data.ShipSlugRelease, nil 306 } 307 308 func (c *GraphQLClient) GetLicense(selector *Selector) (*license, error) { 309 requestObj := GraphQLRequest{ 310 Query: GetLicenseQuery, 311 Variables: map[string]string{ 312 "licenseId": selector.LicenseID, 313 }, 314 } 315 316 ci := callInfo{ 317 username: selector.GetBasicAuthUsername(), 318 password: selector.InstallationID, 319 request: requestObj, 320 upstream: selector.Upstream, 321 } 322 323 licenseResponse := &GQLGetLicenseResponse{} 324 if err := c.callGQL(ci, licenseResponse); err != nil { 325 return nil, err 326 } 327 328 if len(licenseResponse.Errors) > 0 { 329 var multiErr *multierror.Error 330 for _, err := range licenseResponse.Errors { 331 multiErr = multierror.Append(multiErr, fmt.Errorf("%s: %s", err.Code, err.Message)) 332 333 } 334 return nil, multiErr.ErrorOrNil() 335 } 336 337 return &licenseResponse.Data.License, nil 338 } 339 340 func (c *GraphQLClient) RegisterInstall(customerID, installationID, channelID, releaseID string) error { 341 requestObj := GraphQLRequest{ 342 Query: RegisterInstallQuery, 343 Variables: map[string]string{ 344 "channelId": channelID, 345 "releaseId": releaseID, 346 }, 347 } 348 349 ci := callInfo{ 350 username: customerID, 351 password: installationID, 352 request: requestObj, 353 } 354 355 shipResponse := &GQLRegisterInstallResponse{} 356 if err := c.callGQL(ci, shipResponse); err != nil { 357 return err 358 } 359 360 if shipResponse.Errors != nil && len(shipResponse.Errors) > 0 { 361 var multiErr *multierror.Error 362 for _, err := range shipResponse.Errors { 363 multiErr = multierror.Append(multiErr, fmt.Errorf("%s: %s", err.Code, err.Message)) 364 365 } 366 return multiErr.ErrorOrNil() 367 } 368 369 return nil 370 } 371 372 func (c *GraphQLClient) callGQL(ci callInfo, result interface{}) error { 373 body, err := json.Marshal(ci.request) 374 if err != nil { 375 return errors.Wrap(err, "marshal request") 376 } 377 378 bodyReader := ioutil.NopCloser(bytes.NewReader(body)) 379 380 gqlServer := c.GQLServer.String() 381 if ci.upstream != "" { 382 gqlServer = ci.upstream 383 } 384 385 graphQLRequest, err := http.NewRequest(http.MethodPost, gqlServer, bodyReader) 386 if err != nil { 387 return errors.Wrap(err, "create new request") 388 } 389 390 graphQLRequest.Header = map[string][]string{ 391 "Content-Type": {"application/json"}, 392 } 393 394 if ci.username != "" || ci.password != "" { 395 authString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", ci.username, ci.password))) 396 graphQLRequest.Header["Authorization"] = []string{"Basic " + authString} 397 } 398 399 resp, err := c.Client.Do(graphQLRequest) 400 if err != nil { 401 return errors.Wrap(err, "send request") 402 } 403 404 responseBody, err := ioutil.ReadAll(resp.Body) 405 if err != nil { 406 return errors.Wrap(err, "read body") 407 } 408 409 if err := json.Unmarshal(responseBody, result); err != nil { 410 return errors.Wrapf(err, "unmarshal response %s", responseBody) 411 } 412 413 return nil 414 }