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  }