github.com/actions-on-google/gactions@v3.2.0+incompatible/api/sdk.go (about)

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package sdk implements the adapter to talk to Actions SDK server.
    16  package sdk
    17  
    18  import (
    19  	"archive/zip"
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/url"
    29  	"os"
    30  	"path"
    31  	"path/filepath"
    32  	"regexp"
    33  	"runtime"
    34  	"strings"
    35  	"text/tabwriter"
    36  	"time"
    37  
    38  	"github.com/actions-on-google/gactions/api/apiutils"
    39  	"github.com/actions-on-google/gactions/api/request"
    40  	"github.com/actions-on-google/gactions/log"
    41  	"github.com/actions-on-google/gactions/project"
    42  	"github.com/actions-on-google/gactions/project/studio"
    43  	"github.com/actions-on-google/gactions/versions"
    44  	"gopkg.in/yaml.v2"
    45  )
    46  
    47  const (
    48  	actionsProdURL             = "actions.googleapis.com"
    49  	actionsConsoleProdURL      = "console.actions.google.com"
    50  	encryptEndpoint            = "v2:encryptSecret"
    51  	decryptEndpoint            = "v2:decryptSecret"
    52  	listSampleProjectsEndpoint = "v2/sampleProjects"
    53  	// Prod version of CurEnv
    54  	Prod = "prod"
    55  	// ProdChannel of AoG release
    56  	ProdChannel = "actions.channels.Production"
    57  	// AlphaChannel of AoG release
    58  	AlphaChannel = "actions.channels.Alpha"
    59  	// BetaChannel of AoG release
    60  	BetaChannel = "actions.channels.ClosedBeta"
    61  )
    62  
    63  var (
    64  	// CurEnv determines which version of the Actions API to call.
    65  	CurEnv      = Prod
    66  	consoleAddr = "https://" + urlMap[CurEnv]["consoleURL"]
    67  	// Consumer holds the string identifying the caller to Google. This is based on a command line flag.
    68  	Consumer = ""
    69  	// responseBodyReadTimeout is a time limit to read body of HTTP response after response object is received.
    70  	responseBodyReadTimeout = 5 * time.Second
    71  	BuiltInReleaseChannels = map[string]string{
    72  		ProdChannel:     "prod",
    73  	}
    74  )
    75  
    76  var urlMap = map[string]map[string]string{
    77  	Prod: map[string]string{
    78  		"apiURL":     actionsProdURL,
    79  		"consoleURL": actionsConsoleProdURL,
    80  	},
    81  }
    82  
    83  // CreateVersionHTTPResponse represents the expected fields the CLI expects from the CreateVersion API.
    84  // CLI will use those fields to print an output message to a user. All other fields from an API
    85  // response will be ignored.
    86  type CreateVersionHTTPResponse struct {
    87  	Name string `json:"name"`
    88  }
    89  
    90  // WriteDraftHTTPResponse represents the expected fields the CLI expects from the WriteDraft API.
    91  // CLI will use those fields to print an output message to a user. All other fields from an API
    92  // response will be ignored.
    93  type WriteDraftHTTPResponse struct {
    94  	Name              string `json:"name"`
    95  	ValidationResults struct {
    96  		Results []validationResult `json:"results"`
    97  	} `json:"validationResults"`
    98  }
    99  
   100  type validationResult struct {
   101  	ValidationMessage string `json:"validationMessage"`
   102  	ValidationContext struct {
   103  		LanguageCode string `json:"languageCode"`
   104  	} `json:"validationContext"`
   105  }
   106  
   107  // WritePreviewHTTPResponse represents the expected fields the CLI expects from the WritePreview
   108  // API. CLI will use those fields to print an output message to a user. All other fields from
   109  // an API response will be ignored.
   110  type WritePreviewHTTPResponse struct {
   111  	Name              string `json:"name"`
   112  	ValidationResults struct {
   113  		Results []validationResult `json:"results"`
   114  	} `json:"validationResults"`
   115  	SimulatorURL string `json:"simulatorUrl"`
   116  }
   117  
   118  // EncryptSecretHTTPResponse represents the expected fields the CLI expects from the EncryptSecret endpoint.
   119  type EncryptSecretHTTPResponse struct {
   120  	AccountLinkingSecret map[string]interface{} `json:"accountLinkingSecret"`
   121  }
   122  
   123  // PublicError represents a public error structure inside of an API response. All other fields
   124  // (for example, containing the internal details) will be omitted.
   125  type PublicError struct {
   126  	Error struct {
   127  		Code    int                      `json:"code,omitempty"`
   128  		Message string                   `json:"message,omitempty"`
   129  		Details []map[string]interface{} `json:"details,omitempty"`
   130  	} `json:"error,omitempty"`
   131  }
   132  
   133  type configFiles struct {
   134  	ConfigFiles []map[string]interface{} `json:"configFiles"`
   135  }
   136  
   137  type dataFiles struct {
   138  	DataFiles []struct {
   139  		Filepath    string `json:"filePath"`
   140  		Payload     []byte `json:"payload"`
   141  		ContentType string `json:"contentType"`
   142  	} `json:"dataFiles"`
   143  }
   144  
   145  type streamRecord struct {
   146  	Files struct {
   147  		ConfigFiles *configFiles `json:"configFiles"`
   148  		DataFiles   *dataFiles   `json:"dataFiles"`
   149  	} `json:"files"`
   150  }
   151  
   152  func httpAddr(endpoint string) string {
   153  	return "https://" + urlMap[CurEnv]["apiURL"] + "/" + endpoint
   154  }
   155  
   156  func writeDraftHTTPEndpoint(projectID string) string {
   157  	return fmt.Sprintf("v2/projects/%s/draft:write", projectID)
   158  }
   159  
   160  func previewHTTPEndpoint(projectID string) string {
   161  	return fmt.Sprintf("v2/projects/%s/preview:write", projectID)
   162  }
   163  
   164  func versionHTTPEndpoint(projectID string) string {
   165  	return fmt.Sprintf("v2/projects/%s/versions:create", projectID)
   166  }
   167  
   168  func readDraftHTTPEndpoint(projectID string) string {
   169  	return fmt.Sprintf("v2/projects/%s/draft:read", projectID)
   170  }
   171  
   172  func readVersionHTTPEndpoint(projectID, versionID string) string {
   173  	return fmt.Sprintf("v2/projects/%s/versions/%s:read", projectID, versionID)
   174  }
   175  
   176  func listReleaseChannelsHTTPEndpoint(projectID string) string {
   177  	return fmt.Sprintf("v2/projects/%s/releaseChannels", projectID)
   178  }
   179  
   180  func listVersionsHTTPEndpoint(projectID string) string {
   181  	return fmt.Sprintf("v2/projects/%s/versions", projectID)
   182  }
   183  
   184  func check(cfgs map[string][]byte) error {
   185  	if len(cfgs) == 0 {
   186  		return errors.New("configuration files for your Action were not found")
   187  	}
   188  	// base settings file
   189  	if _, ok := cfgs["settings/settings.yaml"]; !ok {
   190  		return errors.New("settings/settings.yaml for your Action was not found")
   191  	}
   192  	if _, ok := cfgs["manifest.yaml"]; !ok {
   193  		return errors.New("manifest.yaml for your Action was not found")
   194  	}
   195  	return nil
   196  }
   197  
   198  func printSize(req map[string]interface{}) {
   199  	b, err := json.Marshal(req)
   200  	if err != nil {
   201  		log.Infof("Tried marshalling request into JSON: %v\n", err)
   202  		return
   203  	}
   204  	log.Infof("Total request size is %v bytes.", len(b))
   205  }
   206  
   207  // sendFilesToServerJSON will stream series of requests based on proj to w.
   208  // The function performs client-side streaming via HTTP/JSON. This is done by
   209  // sending an array of JSON requests.
   210  func sendFilesToServerJSON(p project.Project, w *io.PipeWriter, makeRequest func() map[string]interface{}) (err error) {
   211  	// Important - must close w to avoid deadlock for the reader end of the pipe.
   212  	defer func() {
   213  		// Don't want to overwrite other errors raised in the func.
   214  		// If any other error happened, then the PipeWriter error is not significant.
   215  		err2 := w.Close()
   216  		if err == nil {
   217  			err = err2
   218  		}
   219  	}()
   220  	files, err := p.Files()
   221  	if err != nil {
   222  		return err
   223  	}
   224  	configFiles := studio.ConfigFiles(files)
   225  	dataFiles, err := studio.DataFiles(p)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	if err := check(configFiles); err != nil {
   230  		return err
   231  	}
   232  	encoder := json.NewEncoder(w)
   233  	_, err = w.Write([]byte("["))
   234  	if err != nil {
   235  		return err
   236  	}
   237  	streamer := request.NewStreamer(configFiles, dataFiles, makeRequest, p.ProjectRoot(), request.MaxChunkSizeBytes-request.Padding)
   238  	for streamer.HasNext() {
   239  		req, err := streamer.Next()
   240  		if err != nil {
   241  			return err
   242  		}
   243  		printSize(req)
   244  		if err = encoder.Encode(req); err != nil {
   245  			// Ignore this error because it's possible for this error
   246  			// to happen when server closed the connection (i.e. the read end of the pipe gets closed)
   247  			// due to a failing internal server logic after processing of configuration files.
   248  			log.Infof("Failed to send previous request: %v\n", err)
   249  			return nil
   250  		}
   251  		if streamer.HasNext() {
   252  			if _, err = w.Write([]byte(",")); err != nil {
   253  				// Ignore this error because it's possible for this error
   254  				// to happen when server closed the connection (i.e. the read end of the pipe gets closed)
   255  				// due to a failing internal server logic after processing of configuration files.
   256  				log.Infof("Failed to send previous request: %v\n", err)
   257  				return nil
   258  			}
   259  		}
   260  	}
   261  	if _, err = w.Write([]byte("]")); err != nil {
   262  		// Ignore this error because it's possible for this error
   263  		// to happen when server closed the connection (i.e. the read end of the pipe gets closed)
   264  		// due to a failing internal server logic after processing of the last data file.
   265  		log.Infof("Failed to send previous request: %v\n", err)
   266  		return nil
   267  	}
   268  	return err
   269  }
   270  
   271  // readBodyWithTimeout reads content from body until EOF is encountered, or timer expired.
   272  // Timer starts when this function starts execution.
   273  func readBodyWithTimeout(body io.Reader, timeout time.Duration) ([]byte, error) {
   274  	// buf is initialized with 1 character to ensure a caller (Read) doesn't wait
   275  	// for EOF to be sent from server.
   276  	buf := make([]byte, 1)
   277  	jsonString := ""
   278  	// Buffered channels should protect against leaked go-routines.
   279  	errCh := make(chan error, 1)
   280  	go func() {
   281  		for {
   282  			n, err := body.Read(buf)
   283  			if n > 0 {
   284  				jsonString += string(buf)
   285  			}
   286  			if err != nil {
   287  				errCh <- err
   288  				break
   289  			}
   290  		}
   291  	}()
   292  	select {
   293  	case <-time.After(timeout):
   294  		return []byte(jsonString), nil
   295  	case err := <-errCh:
   296  		if err == io.EOF {
   297  			return []byte(jsonString), nil
   298  		}
   299  		return nil, err
   300  	}
   301  }
   302  
   303  // postprocessJSONResponse performs error handling of the JSON response, and also processes
   304  // specific fields from the response body based on a callback function.
   305  func postprocessJSONResponse(resp *http.Response, errCh chan error, proc func(body []byte) error) {
   306  	body, err := readBodyWithTimeout(resp.Body, responseBodyReadTimeout)
   307  	if err != nil {
   308  		errCh <- err
   309  		return
   310  	}
   311  	if resp.StatusCode != 200 {
   312  		errCh <- parseError(body)
   313  		return
   314  	}
   315  	// proc should perform a response specific processing; e.g. extracting specific fields. Only relevant if
   316  	// if response code is 200.
   317  	if err := proc(body); err != nil {
   318  		errCh <- err
   319  	}
   320  	errCh <- nil
   321  }
   322  
   323  func parseError(body []byte) error {
   324  	log.Debugln(string(body))
   325  	publicError := &PublicError{}
   326  	if err := json.NewDecoder(bytes.NewReader(body)).Decode(publicError); err != nil {
   327  		// This means the error is not a JSON. This happens when the API URL is malformed, and
   328  		// one platform returns an HTML response. In this case, we print the HTML and disregard the json decoding error.
   329  		return fmt.Errorf(string(body))
   330  	}
   331  	return fmt.Errorf("Server did not return HTTP 200.\n%v", errorMessage(publicError))
   332  }
   333  
   334  func errorMessage(in *PublicError) string {
   335  	out := PublicError{}
   336  	// Only allow details to be surfaced if the error code is 400.
   337  	// 400 corresponds to gRPC FAILED_PRECONDITION and INVALID_ARGUMENT
   338  	switch in.Error.Code {
   339  	case 400:
   340  		out.Error = in.Error
   341  	// 403 is returned when user denied the permission to use API, which
   342  	// is the case when they need to enable the API. The error message
   343  	// contains a helpful info, including the link to the API manager.
   344  	case 403, 404:
   345  		out.Error.Message = in.Error.Message
   346  		out.Error.Code = in.Error.Code
   347  	default:
   348  		out.Error.Message = "Internal error occurred"
   349  		out.Error.Code = in.Error.Code
   350  	}
   351  	b, err := json.MarshalIndent(out, "", "  ")
   352  	if err != nil {
   353  		log.Warnf("%v\n", err)
   354  		return ""
   355  	}
   356  	return string(b)
   357  }
   358  
   359  func printValidationResults(results []validationResult) {
   360  	w := new(tabwriter.Writer)
   361  	w.Init(os.Stdout, 2, 4, 2, ' ', 0)
   362  	fmt.Fprintln(w, "  Locale\tValidation Result\t")
   363  	for _, v := range results {
   364  		fmt.Fprintf(w, "  %v\t%v\t\n", v.ValidationContext.LanguageCode, v.ValidationMessage)
   365  	}
   366  	fmt.Fprint(w)
   367  	w.Flush()
   368  }
   369  
   370  func procWriteDraftResponse(body []byte) error {
   371  	resp := &WriteDraftHTTPResponse{}
   372  	if err := json.NewDecoder(bytes.NewReader(body)).Decode(resp); err != nil {
   373  		return errors.New(string(body))
   374  	}
   375  	if len(resp.ValidationResults.Results) > 0 {
   376  		log.Warnln("Server found validation issues (however, your files were still pushed):")
   377  		printValidationResults(resp.ValidationResults.Results)
   378  	}
   379  	return nil
   380  }
   381  
   382  // WriteDraftJSON implements WriteDraft functionality of the SDK server via HTTP/JSON streaming.
   383  func WriteDraftJSON(ctx context.Context, proj project.Project) error {
   384  	clientSecret, err := proj.ClientSecretJSON()
   385  	if err != nil {
   386  		return err
   387  	}
   388  	client, err := apiutils.NewHTTPClient(
   389  		ctx,
   390  		clientSecret,
   391  		"",
   392  	)
   393  	if err != nil {
   394  		return err
   395  	}
   396  	projectID := proj.ProjectID()
   397  	log.Outf("Pushing files in the project %q to Actions Console. This may take a few minutes.\n", projectID)
   398  	requestURL := httpAddr(writeDraftHTTPEndpoint(projectID))
   399  	r, w := io.Pipe()
   400  	errCh := make(chan error, 1)
   401  	// This goroutine will exit after HTTP call is finished.
   402  	// The sendFilesToServerJSON below and client.Post communicate via the pipe
   403  	// and former will keep writing stream of bytes, which client post will
   404  	// keep reading in a blocking fashion. sendFilesToServerJSON is guaranteed
   405  	// to close the writer end of the pipe, thus unblocking the reader and allowing
   406  	// the goroutine to exit.
   407  	go func() {
   408  		req, err := http.NewRequest("POST", requestURL, r)
   409  		if err != nil {
   410  			errCh <- err
   411  			return
   412  		}
   413  		req.Header.Add("Content-Type", "application/json")
   414  		// This is done to help server to select the quota attributed to a
   415  		// projectID (i.e. developer's project), instead of the CLI project.
   416  		req.Header.Add("X-Goog-User-Project", projectID)
   417  		addClientHeaders(req)
   418  
   419  		resp, err := client.Do(req)
   420  
   421  		if err != nil {
   422  			errCh <- err
   423  			return
   424  		}
   425  		defer resp.Body.Close()
   426  		postprocessJSONResponse(resp, errCh, func(body []byte) error {
   427  			return procWriteDraftResponse(body)
   428  		})
   429  	}()
   430  	if err := sendFilesToServerJSON(proj, w, func() map[string]interface{} {
   431  		return request.WriteDraft(projectID)
   432  	}); err != nil {
   433  		return err
   434  	}
   435  	log.Outf("Waiting for server to respond...")
   436  	err = <-errCh
   437  	if err != nil {
   438  		return err
   439  	}
   440  	log.DoneMsgln(fmt.Sprintf(`Files were pushed to Actions Console, and you can now view your project with this URL: %v/project/%v/overview. If you want to test your changes, run "gactions deploy preview", or navigate to the Test section in the Console.`, consoleAddr, projectID))
   441  	return nil
   442  }
   443  
   444  func procWritePreviewResponse(body []byte) (string, error) {
   445  	resp := &WritePreviewHTTPResponse{}
   446  	if err := json.NewDecoder(bytes.NewReader(body)).Decode(resp); err != nil {
   447  		return "", errors.New(string(body))
   448  	}
   449  	if len(resp.ValidationResults.Results) > 0 {
   450  		log.Warnln("Server found validation issues (however, your files were still pushed):")
   451  		printValidationResults(resp.ValidationResults.Results)
   452  	}
   453  	simulatorURL := resp.SimulatorURL
   454  	if simulatorURL == "" {
   455  		log.Warnf("The API response body doesn't contain the simulator link.")
   456  	}
   457  	return simulatorURL, nil
   458  }
   459  
   460  // WritePreviewJSON implements WritePreview functionality of the SDK server via HTTP/JSON streaming.
   461  func WritePreviewJSON(ctx context.Context, proj project.Project, sandbox bool) error {
   462  	clientSecret, err := proj.ClientSecretJSON()
   463  	if err != nil {
   464  		return err
   465  	}
   466  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
   467  	if err != nil {
   468  		return err
   469  	}
   470  	projectID := proj.ProjectID()
   471  	log.Outf("Deploying files in the project %q to Actions Console for preview. This may take a few minutes.\n", projectID)
   472  	requestURL := httpAddr(previewHTTPEndpoint(projectID))
   473  	r, w := io.Pipe()
   474  	errCh := make(chan error, 1)
   475  	var simulatorURL string
   476  	// This goroutine will exit after HTTP call is finished.
   477  	// The sendFilesToServerJSON below and client.Post communicate via the pipe
   478  	// and former will keep writing stream of bytes, which client post will
   479  	// keep reading in a blocking fashion. sendFilesToServerJSON is guaranteed
   480  	// to close the writer end of the pipe, thus unblocking the reader and allowing
   481  	// the goroutine to exit.
   482  	go func() {
   483  		req, err := http.NewRequest("POST", requestURL, r)
   484  		if err != nil {
   485  			errCh <- err
   486  			return
   487  		}
   488  		req.Header.Add("Content-Type", "application/json")
   489  		// This is done to help server select the quota attributed to a
   490  		// projectID (i.e. developer's project), instead of the CLI project.
   491  		// https://cloud.google.com/storage/docs/xml-api/reference-headers#xgooguserproject
   492  		req.Header.Add("X-Goog-User-Project", projectID)
   493  		// Sets timeout because Cloud Function deployment can take 1-2 minutes.
   494  		const timeoutSec = "180"
   495  		req.Header.Add("X-Server-Timeout", fmt.Sprintf("%v", timeoutSec))
   496  		addClientHeaders(req)
   497  
   498  		resp, err := client.Do(req)
   499  		if err != nil {
   500  			errCh <- err
   501  			return
   502  		}
   503  		defer resp.Body.Close()
   504  		postprocessJSONResponse(resp, errCh, func(body []byte) error {
   505  			v, err := procWritePreviewResponse(body)
   506  			simulatorURL = v
   507  			return err
   508  		})
   509  	}()
   510  	if err := sendFilesToServerJSON(proj, w, func() map[string]interface{} {
   511  		return request.WritePreview(projectID, sandbox)
   512  	}); err != nil {
   513  		return err
   514  	}
   515  	log.Outf("Waiting for server to respond. It could take up to 1 minute if your cloud function needs to be redeployed.")
   516  	err = <-errCh
   517  	if err != nil {
   518  		return err
   519  	}
   520  	log.DoneMsgln(fmt.Sprintf("You can now test your changes in Simulator with this URL: %s", simulatorURL))
   521  	return nil
   522  }
   523  
   524  func procCreateVersionResponse(channel string, body []byte) (string, error) {
   525  	resp := &CreateVersionHTTPResponse{}
   526  	if err := json.NewDecoder(bytes.NewReader(body)).Decode(resp); err != nil {
   527  		return "", errors.New(string(body))
   528  	}
   529  	versionIDRegExp := regexp.MustCompile("^projects/[^//]+/versions/(?P<versionID>[^//]+)$")
   530  	if versionIDMatch := versionIDRegExp.FindStringSubmatch(resp.Name); versionIDMatch == nil {
   531  		log.Debugln(fmt.Sprintf("version id absent in the response %s returned from the server ", resp.Name))
   532  		return "", nil
   533  	}
   534  	return versionIDRegExp.FindStringSubmatch(resp.Name)[versionIDRegExp.SubexpIndex("versionID")], nil
   535  }
   536  
   537  // CreateVersionJSON implements CreateVersion functionality of the SDK server via HTTP/JSON streaming.
   538  func CreateVersionJSON(ctx context.Context, proj project.Project, channel string) error {
   539  	clientSecret, err := proj.ClientSecretJSON()
   540  	if err != nil {
   541  		return err
   542  	}
   543  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
   544  	if err != nil {
   545  		return err
   546  	}
   547  	projectID := proj.ProjectID()
   548  	log.Outf("Deploying files in the project %q to the %q release channel...", projectID, channel)
   549  	requestURL := httpAddr(versionHTTPEndpoint(projectID))
   550  	r, w := io.Pipe()
   551  	errCh := make(chan error, 1)
   552  	var versionID string
   553  	// This goroutine will exit after HTTP call is finished.
   554  	// The sendFilesToServerJSON below and client.Post communicate via the pipe
   555  	// and former will keep writing stream of bytes, which client post will
   556  	// keep reading in a blocking fashion. sendFilesToServerJSON is guaranteed
   557  	// to close the writer end of the pipe, thus unblocking the reader and allowing
   558  	// the goroutine to exit.
   559  	go func() {
   560  		req, err := http.NewRequest("POST", requestURL, r)
   561  		if err != nil {
   562  			errCh <- err
   563  			return
   564  		}
   565  		req.Header.Add("Content-Type", "application/json")
   566  		// This is done to help server select the quota attributed to a
   567  		// projectID (i.e. developer's project), instead of the CLI project.
   568  		// https://cloud.google.com/storage/docs/xml-api/reference-headers#xgooguserproject
   569  		req.Header.Add("X-Goog-User-Project", projectID)
   570  		addClientHeaders(req)
   571  
   572  		resp, err := client.Do(req)
   573  		if err != nil {
   574  			errCh <- err
   575  			return
   576  		}
   577  		defer resp.Body.Close()
   578  		// TODO: Change signature of postProcessJSONResponse to return an error, and pipe that error to channel here.
   579  		postprocessJSONResponse(resp, errCh, func(body []byte) error {
   580  			v, err := procCreateVersionResponse(channel, body)
   581  			versionID = v
   582  			return err
   583  		})
   584  	}()
   585  	if err := sendFilesToServerJSON(proj, w, func() map[string]interface{} {
   586  		return request.CreateVersion(projectID, channel)
   587  	}); err != nil {
   588  		return err
   589  	}
   590  	log.Outf("Waiting for server to respond...")
   591  	if err := <-errCh; err != nil {
   592  		return err
   593  	}
   594  	if _, ok := BuiltInReleaseChannels[channel]; ok {
   595  		channel = BuiltInReleaseChannels[channel]
   596  	}
   597  
   598  	log.DoneMsgln(fmt.Sprintf("Version %s has been successfully created and submitted for deployment to %s channel. ", versionID, channel))
   599  	return nil
   600  }
   601  
   602  func keyInConfigResp(path string) (string, error) {
   603  	var k string
   604  	switch {
   605  	case studio.IsWebhookDefinition(path):
   606  		k = "webhook"
   607  	case studio.IsVertical(path):
   608  		k = "verticalSettings"
   609  	case studio.IsManifest(path):
   610  		k = "manifest"
   611  	case studio.IsActions(path):
   612  		k = "actions"
   613  	case studio.IsIntent(path):
   614  		k = "intent"
   615  	case studio.IsGlobal(path):
   616  		k = "globalIntentEvent"
   617  	case studio.IsScene(path):
   618  		k = "scene"
   619  	case studio.IsType(path):
   620  		k = "type"
   621  	case studio.IsEntitySet(path):
   622  		k = "entitySet"
   623  	case studio.IsPrompt(path):
   624  		k = "staticPrompt"
   625  	case studio.IsDeviceFulfillment(path):
   626  		k = "deviceFulfillment"
   627  	case studio.IsResourceBundle(path):
   628  		k = "resourceBundle"
   629  	case studio.IsSettings(path):
   630  		k = "settings"
   631  	case studio.IsAccountLinkingSecret(path):
   632  		k = "accountLinkingSecret"
   633  	default:
   634  		return k, fmt.Errorf("%v is unknown config file type to CLI", path)
   635  	}
   636  	return k, nil
   637  }
   638  
   639  func receiveConfigFiles(proj project.Project, cfgs *configFiles, force bool, seen map[string]bool) error {
   640  	for _, cfg := range cfgs.ConfigFiles {
   641  		p, ok := cfg["filePath"]
   642  		if !ok {
   643  			return fmt.Errorf("%v doesn't have required filePath field", cfg)
   644  		}
   645  		path, ok := p.(string)
   646  		if !ok {
   647  			return fmt.Errorf("%v has a key of %v of incorrect type %T, want string", cfg, p, p)
   648  		}
   649  		k, err := keyInConfigResp(path)
   650  		if err != nil {
   651  			return err
   652  		}
   653  		v := cfg[k]
   654  		// Transform v into YAML.
   655  		mp, ok := v.(map[string]interface{})
   656  		if !ok {
   657  			return fmt.Errorf("%v has a key %v of incorrect type %T", cfg, v, v)
   658  		}
   659  		b, err := yaml.Marshal(mp)
   660  		if err != nil {
   661  			return err
   662  		}
   663  		// TODO: Can be spun as go-routine.
   664  		if err := studio.WriteToDisk(proj, path, "", b, force); err != nil {
   665  			return err
   666  		}
   667  		seen[path] = true
   668  	}
   669  	return nil
   670  }
   671  
   672  func receiveDataFiles(proj project.Project, dfs *dataFiles, force bool, seen map[string]bool) error {
   673  	for _, df := range dfs.DataFiles {
   674  		if err := studio.WriteToDisk(proj, df.Filepath, df.ContentType, df.Payload, force); err != nil {
   675  			return err
   676  		}
   677  		if df.ContentType != "application/zip;zip_type=cloud_function" {
   678  			seen[df.Filepath] = true
   679  			continue
   680  		}
   681  		// WriteToDisk will unzip cloud function folder. Need to record the names of extracted files.
   682  		names, err := namesFromZip(df.Payload)
   683  		if err != nil {
   684  			return err
   685  		}
   686  		for _, v := range names {
   687  			fp := path.Join(df.Filepath[:len(df.Filepath)-len(".zip")], v)
   688  			seen[fp] = true
   689  		}
   690  	}
   691  	return nil
   692  }
   693  
   694  func receiveStream(proj project.Project, body io.Reader, force bool, seen map[string]bool) error {
   695  	dec := json.NewDecoder(body)
   696  	log.Debugln("Starts processing the stream")
   697  	// Reads "[".
   698  	t, err := dec.Token()
   699  	if err != nil {
   700  		return err
   701  	}
   702  	if t != json.Delim('[') {
   703  		return fmt.Errorf("expected [ got %v", t)
   704  	}
   705  	for dec.More() {
   706  		var rec streamRecord
   707  		if err := dec.Decode(&rec); err != nil {
   708  			return err
   709  		}
   710  		if rec.Files.ConfigFiles != nil {
   711  			if err := receiveConfigFiles(proj, rec.Files.ConfigFiles, force, seen); err != nil {
   712  				return err
   713  			}
   714  		}
   715  		if rec.Files.DataFiles != nil {
   716  			if err := receiveDataFiles(proj, rec.Files.DataFiles, force, seen); err != nil {
   717  				return err
   718  			}
   719  		}
   720  	}
   721  	// Reads "]".
   722  	t, err = dec.Token()
   723  	if err != nil {
   724  		return err
   725  	}
   726  	if t != json.Delim(']') {
   727  		return fmt.Errorf("expected ] got %v", t)
   728  	}
   729  	log.Debugln("Finished processing the stream")
   730  	return nil
   731  }
   732  
   733  func namesFromZip(content []byte) ([]string, error) {
   734  	r, err := zip.NewReader(bytes.NewReader(content), int64(len(content)))
   735  	if err != nil {
   736  		return nil, err
   737  	}
   738  	var names []string
   739  	for _, f := range r.File {
   740  		names = append(names, f.Name)
   741  	}
   742  	return names, nil
   743  }
   744  
   745  func findExtra(a map[string][]byte, b map[string]bool) []string {
   746  	u := map[string]bool{}
   747  	for k := range a {
   748  		u[k] = true
   749  	}
   750  	for k := range b {
   751  		u[k] = true
   752  	}
   753  	// flag f in a, that are not in b
   754  	var extra []string
   755  	for k := range u {
   756  		if _, ok := b[k]; !ok {
   757  			extra = append(extra, k)
   758  		}
   759  	}
   760  	return extra
   761  }
   762  
   763  func addClientHeaders(req *http.Request) {
   764  	if Consumer != "" {
   765  		req.Header.Add("Gactions-Consumer", Consumer)
   766  	}
   767  	ua := fmt.Sprintf("gactions/%s (%s %s)", versions.CliVersion, runtime.GOOS, runtime.GOARCH)
   768  	req.Header.Add("User-Agent", ua)
   769  }
   770  
   771  func parseEncryptionKeyVersion(files map[string][]byte) string {
   772  	type secretFile struct {
   773  		EncryptionKeyVersion string `yaml:"encryptionKeyVersion"`
   774  	}
   775  	in, ok := files["settings/accountLinkingSecret.yaml"]
   776  	if !ok {
   777  		return ""
   778  	}
   779  	f := secretFile{}
   780  	if err := yaml.Unmarshal(in, &f); err != nil {
   781  		return ""
   782  	}
   783  	return f.EncryptionKeyVersion
   784  }
   785  
   786  // ReadDraftJSON implements ReadDraft functionality of SDK server.
   787  func ReadDraftJSON(ctx context.Context, proj project.Project, force bool, clean bool) error {
   788  	client, err := setupClient(ctx, proj)
   789  	if err != nil {
   790  		return err
   791  	}
   792  	projectID := proj.ProjectID()
   793  	log.Outf("Pulling files in the project %q from Actions Console...\n", projectID)
   794  	requestURL := httpAddr(readDraftHTTPEndpoint(projectID))
   795  	warn := "%v is not present in the draft of your Action"
   796  	files, err := proj.Files()
   797  	if err != nil {
   798  		return err
   799  	}
   800  	body, err := json.Marshal(request.ReadDraft(projectID, parseEncryptionKeyVersion(files)))
   801  	if err != nil {
   802  		return err
   803  	}
   804  	return sendRequest(client, requestURL, body, files, proj, warn, force, clean)
   805  }
   806  
   807  func procEncryptSecretResponse(proj project.Project, body []byte) error {
   808  	r := EncryptSecretHTTPResponse{}
   809  	if err := json.Unmarshal(body, &r); err != nil {
   810  		return err
   811  	}
   812  	b, err := yaml.Marshal(r.AccountLinkingSecret)
   813  	if err != nil {
   814  		return err
   815  	}
   816  	if err := studio.WriteToDisk(proj, "settings/accountLinkingSecret.yaml", "", b, false); err != nil {
   817  		return err
   818  	}
   819  	log.DoneMsgln(fmt.Sprintf("Encrypted secret is in %s", filepath.Join(proj.ProjectRoot(), "settings", "accountLinkingSecret.yaml")))
   820  	return nil
   821  }
   822  
   823  // EncryptSecretJSON implements Encrypt functionality of SDK server.
   824  func EncryptSecretJSON(ctx context.Context, proj project.Project, secret string) error {
   825  	clientSecret, err := proj.ClientSecretJSON()
   826  	if err != nil {
   827  		return err
   828  	}
   829  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
   830  	if err != nil {
   831  		return err
   832  	}
   833  	log.Outf("Encrypting your client secret...")
   834  	// Using a channel and goroutine is not ideal here, but this allows one to
   835  	// reuse postprocessJSONResponse function.
   836  	// Should to refactor postprocessJSONResponse to avoid channels.
   837  	errCh := make(chan error, 1)
   838  	go func() {
   839  		requestURL := httpAddr(encryptEndpoint)
   840  		body, err := json.Marshal(request.EncryptSecret(secret))
   841  		if err != nil {
   842  			errCh <- err
   843  		}
   844  		req, err := http.NewRequest("POST", requestURL, bytes.NewReader(body))
   845  		if err != nil {
   846  			errCh <- err
   847  		}
   848  		req.Header.Add("Content-Type", "application/json")
   849  		addClientHeaders(req)
   850  		resp, err := client.Do(req)
   851  		if err != nil {
   852  			errCh <- err
   853  		}
   854  		defer resp.Body.Close()
   855  		postprocessJSONResponse(resp, errCh, func(body []byte) error {
   856  			return procEncryptSecretResponse(proj, body)
   857  		})
   858  	}()
   859  	if err := <-errCh; err != nil {
   860  		return err
   861  	}
   862  	return nil
   863  }
   864  
   865  func procDecryptSecretResponse(proj project.Project, body []byte, out string) error {
   866  	type resp struct {
   867  		ClientSecret string `json:"clientSecret"`
   868  	}
   869  	r := resp{}
   870  	if err := json.Unmarshal(body, &r); err != nil {
   871  		return err
   872  	}
   873  	rel, err := filepath.Rel(proj.ProjectRoot(), out)
   874  	if err != nil {
   875  		return err
   876  	}
   877  	if err := studio.WriteToDisk(proj, rel, "", []byte(r.ClientSecret), false); err != nil {
   878  		return err
   879  	}
   880  	log.Warnf("Decrypted key will be stored at %s. Committing this file to source control is not recommend.\n", out)
   881  	log.DoneMsgln(fmt.Sprintf("Decrypted client secret key is in %s.", out))
   882  	return nil
   883  }
   884  
   885  // DecryptSecretJSON implements Decrypt functionality of SDK server.
   886  func DecryptSecretJSON(ctx context.Context, proj project.Project, secret string, out string) error {
   887  	clientSecret, err := proj.ClientSecretJSON()
   888  	if err != nil {
   889  		return err
   890  	}
   891  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
   892  	if err != nil {
   893  		return err
   894  	}
   895  	log.Outf("Decrypting your client secret...")
   896  	requestURL := httpAddr(decryptEndpoint)
   897  	body, err := json.Marshal(request.DecryptSecret(secret))
   898  	if err != nil {
   899  		return err
   900  	}
   901  	req, err := http.NewRequest("POST", requestURL, bytes.NewReader(body))
   902  	if err != nil {
   903  		return err
   904  	}
   905  	req.Header.Add("Content-Type", "application/json")
   906  	addClientHeaders(req)
   907  	resp, err := client.Do(req)
   908  	if err != nil {
   909  		return err
   910  	}
   911  	defer resp.Body.Close()
   912  	// Using a channel and goroutine is not ideal here, but this allows one to
   913  	// reuse postprocessJSONResponse function.
   914  	// Should to refactor postprocessJSONResponse to avoid channels.
   915  	errCh := make(chan error, 1)
   916  	postprocessJSONResponse(resp, errCh, func(body []byte) error {
   917  		return procDecryptSecretResponse(proj, body, out)
   918  	})
   919  	return <-errCh
   920  }
   921  
   922  func sendListRequest(pageToken, requestURL string, client *http.Client) ([]byte, error) {
   923  	// List API must not have a body, so encoding request fields into a URL.
   924  	u, err := url.Parse(requestURL)
   925  	if err != nil {
   926  		return nil, err
   927  	}
   928  	q := u.Query()
   929  	q.Set("pageToken", pageToken)
   930  	u.RawQuery = q.Encode()
   931  	requestURL = u.String()
   932  	req, err := http.NewRequest("GET", requestURL, nil)
   933  	if err != nil {
   934  		return nil, err
   935  	}
   936  	addClientHeaders(req)
   937  	resp, err := client.Do(req)
   938  	if err != nil {
   939  		return nil, err
   940  	}
   941  	defer resp.Body.Close()
   942  	body, err := ioutil.ReadAll(resp.Body)
   943  	if err != nil {
   944  		return nil, err
   945  	}
   946  	if resp.StatusCode != 200 {
   947  		return nil, parseError(body)
   948  	}
   949  	return body, nil
   950  }
   951  
   952  // ListSampleProjectsJSON implements ListSampleProjects endpoint of SDK server.
   953  func ListSampleProjectsJSON(ctx context.Context, proj project.Project) ([]project.SampleProject, error) {
   954  	clientSecret, err := proj.ClientSecretJSON()
   955  	if err != nil {
   956  		return nil, err
   957  	}
   958  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
   959  	if err != nil {
   960  		return nil, err
   961  	}
   962  	requestURL := httpAddr(listSampleProjectsEndpoint)
   963  	var res []project.SampleProject
   964  	pageToken := ""
   965  
   966  	for {
   967  		body, err := sendListRequest(pageToken, requestURL, client)
   968  		if err != nil {
   969  			return nil, err
   970  		}
   971  		type listSampleProjectsResponse struct {
   972  			SampleProjects []project.SampleProject `json:"sampleProjects"`
   973  			NextPageToken  string                  `json:"nextPageToken"`
   974  		}
   975  		r := listSampleProjectsResponse{}
   976  		if err = json.Unmarshal(body, &r); err != nil {
   977  			return nil, err
   978  		}
   979  		pageToken = r.NextPageToken
   980  		for _, v := range r.SampleProjects {
   981  			// API returns sampleProjects/{sampleName}.
   982  			v.Name = strings.TrimPrefix(v.Name, "sampleProjects/")
   983  			res = append(res, v)
   984  		}
   985  		if pageToken == "" {
   986  			break
   987  		}
   988  	}
   989  	return res, nil
   990  }
   991  
   992  // ReadVersionJSON implements ReadVersion functionality of SDK server.
   993  func ReadVersionJSON(ctx context.Context, proj project.Project, force bool, clean bool, versionID string) error {
   994  	client, err := setupClient(ctx, proj)
   995  	if err != nil {
   996  		return err
   997  	}
   998  
   999  	projectID := proj.ProjectID()
  1000  	log.Outf("Pulling version %q of the project %q from Actions Console...\n", versionID, projectID)
  1001  	requestURL := httpAddr(readVersionHTTPEndpoint(projectID, versionID))
  1002  	warning := "%v is not present in the version of your Action"
  1003  
  1004  	files, err := proj.Files()
  1005  	if err != nil {
  1006  		return err
  1007  	}
  1008  	body, err := json.Marshal(request.ReadVersion(projectID, parseEncryptionKeyVersion(files)))
  1009  	if err != nil {
  1010  		return err
  1011  	}
  1012  
  1013  	return sendRequest(client, requestURL, body, files, proj, warning, force, clean)
  1014  }
  1015  
  1016  func setupClient(ctx context.Context, proj project.Project) (*http.Client, error) {
  1017  	clientSecret, err := proj.ClientSecretJSON()
  1018  	if err != nil {
  1019  		return nil, err
  1020  	}
  1021  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
  1022  	if err != nil {
  1023  		return nil, err
  1024  	}
  1025  	return client, nil
  1026  }
  1027  
  1028  func sendRequest(client *http.Client, requestURL string, body []byte, files map[string][]byte, proj project.Project, warning string, force, clean bool) error {
  1029  	projectID := proj.ProjectID()
  1030  
  1031  	req, err := http.NewRequest("POST", requestURL, bytes.NewReader(body))
  1032  	if err != nil {
  1033  		return err
  1034  	}
  1035  	req.Header.Add("Content-Type", "application/json")
  1036  	// This is done to help server select the quota attributed to a
  1037  	// projectID (i.e. developer's project), instead of the CLI project.
  1038  	// https://cloud.google.com/storage/docs/xml-api/reference-headers#xgooguserproject
  1039  	req.Header.Add("X-Goog-User-Project", projectID)
  1040  	addClientHeaders(req)
  1041  	resp, err := client.Do(req)
  1042  	if err != nil {
  1043  		return err
  1044  	}
  1045  	defer resp.Body.Close()
  1046  	if resp.StatusCode != 200 {
  1047  		// In case of an error, it's okay to read entire response body because
  1048  		// it will be small.
  1049  		body, err := readBodyWithTimeout(resp.Body, responseBodyReadTimeout)
  1050  		if err != nil {
  1051  			return err
  1052  		}
  1053  		log.Debugln(string(body))
  1054  		publicErrors := []PublicError{}
  1055  		if err := json.NewDecoder(bytes.NewReader(body)).Decode(&publicErrors); err != nil {
  1056  			// This means the error is not a JSON. This happens when the API URL is malformed, and
  1057  			// one platform returns an HTML response. In this case, we print the HTML and disregard the json decoding error.
  1058  			return fmt.Errorf(string(body))
  1059  		}
  1060  		if len(publicErrors) > 0 {
  1061  			return fmt.Errorf("server did not return HTTP 200\n%v", errorMessage(&publicErrors[0]))
  1062  		}
  1063  		return errors.New("server did not return HTTP 200")
  1064  	}
  1065  	seen := map[string]bool{}
  1066  	if err := receiveStream(proj, resp.Body, force, seen); err != nil {
  1067  		return err
  1068  	}
  1069  	extra := findExtra(files, seen)
  1070  	for _, v := range extra {
  1071  		fp := filepath.Join(proj.ProjectRoot(), filepath.FromSlash(v))
  1072  		warn := fmt.Sprintf(warning, fp)
  1073  		if clean {
  1074  			log.Warnf("%v. Removing %v.\n", warn, fp)
  1075  			if err := os.RemoveAll(fp); err != nil {
  1076  				return err
  1077  			}
  1078  		} else {
  1079  			log.Warnf("%v. To remove, run pull with --clean flag.\n", warn)
  1080  		}
  1081  	}
  1082  	return nil
  1083  }
  1084  
  1085  // ListReleaseChannelsJSON implements ListReleaseChannels endpoint of SDK server.
  1086  func ListReleaseChannelsJSON(ctx context.Context, proj project.Project) ([]project.ReleaseChannel, error) {
  1087  	clientSecret, err := proj.ClientSecretJSON()
  1088  	if err != nil {
  1089  		return nil, err
  1090  	}
  1091  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
  1092  	if err != nil {
  1093  		return nil, err
  1094  	}
  1095  	requestURL := httpAddr(listReleaseChannelsHTTPEndpoint(proj.ProjectID()))
  1096  	var res []project.ReleaseChannel
  1097  	pageToken := ""
  1098  
  1099  	for {
  1100  		body, err := sendListRequest(pageToken, requestURL, client)
  1101  		if err != nil {
  1102  			return nil, err
  1103  		}
  1104  		type listReleaseChannelsResponse struct {
  1105  			ReleaseChannels []project.ReleaseChannel `json:"releaseChannels"`
  1106  			NextPageToken   string                   `json:"nextPageToken"`
  1107  		}
  1108  		r := listReleaseChannelsResponse{}
  1109  		if err = json.Unmarshal(body, &r); err != nil {
  1110  			return nil, err
  1111  		}
  1112  		pageToken = r.NextPageToken
  1113  		for _, v := range r.ReleaseChannels {
  1114  			// API returns releaseChannels/{releaseChannelName}.
  1115  			v.Name = strings.TrimPrefix(v.Name, "releaseChannels/")
  1116  			res = append(res, v)
  1117  		}
  1118  		if pageToken == "" {
  1119  			break
  1120  		}
  1121  	}
  1122  	return res, nil
  1123  }
  1124  
  1125  // ListVersionsJSON implements ListVersions endpoint of SDK server.
  1126  func ListVersionsJSON(ctx context.Context, proj project.Project) ([]project.Version, error) {
  1127  	clientSecret, err := proj.ClientSecretJSON()
  1128  	if err != nil {
  1129  		return nil, err
  1130  	}
  1131  	client, err := apiutils.NewHTTPClient(ctx, clientSecret, "")
  1132  	if err != nil {
  1133  		return nil, err
  1134  	}
  1135  	requestURL := httpAddr(listVersionsHTTPEndpoint(proj.ProjectID()))
  1136  	var res []project.Version
  1137  	pageToken := ""
  1138  
  1139  	for {
  1140  		body, err := sendListRequest(pageToken, requestURL, client)
  1141  		if err != nil {
  1142  			return nil, err
  1143  		}
  1144  		type listVersionsResponse struct {
  1145  			Versions      []project.Version `json:"versions"`
  1146  			NextPageToken string            `json:"nextPageToken"`
  1147  		}
  1148  		r := listVersionsResponse{}
  1149  		if err := json.Unmarshal(body, &r); err != nil {
  1150  			return nil, err
  1151  		}
  1152  		pageToken = r.NextPageToken
  1153  		for _, v := range r.Versions {
  1154  			// API returns versions/{versionName}.
  1155  			v.ID = strings.TrimPrefix(v.ID, "versions/")
  1156  			res = append(res, v)
  1157  		}
  1158  		if pageToken == "" {
  1159  			break
  1160  		}
  1161  	}
  1162  	return res, nil
  1163  }