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  }