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