github.com/jfrog/jfrog-cli-platform-services@v1.2.0/commands/commands_commons.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"os"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    16  
    17  	"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
    18  	"github.com/jfrog/jfrog-client-go/utils"
    19  	"github.com/jfrog/jfrog-client-go/utils/log"
    20  
    21  	"github.com/jfrog/jfrog-cli-platform-services/model"
    22  )
    23  
    24  // Useful to capture output in tests
    25  var (
    26  	cliOut        io.Writer = os.Stdout
    27  	cliIn         io.Reader = os.Stdin
    28  	importPattern           = regexp.MustCompile(`(?ms)^\s*(import\s+[^;]+;\s*)(.*)$`)
    29  )
    30  
    31  func prettifyJson(in []byte) []byte {
    32  	var out bytes.Buffer
    33  	if err := json.Indent(&out, in, "", "  "); err != nil {
    34  		return in
    35  	}
    36  	return out.Bytes()
    37  }
    38  
    39  func outputApiResponse(res *http.Response, okStatus int) error {
    40  	return processApiResponse(res, func(responseBytes []byte, statusCode int) error {
    41  		var err error
    42  
    43  		if res.StatusCode != okStatus {
    44  			err = fmt.Errorf("command failed with status %d", res.StatusCode)
    45  		}
    46  
    47  		if err == nil {
    48  			_, err = cliOut.Write(prettifyJson(responseBytes))
    49  		} else if len(responseBytes) > 0 {
    50  			// We will report the previous error, but we still want to display the response body
    51  			if _, writeErr := cliOut.Write(prettifyJson(responseBytes)); writeErr != nil {
    52  				log.Debug(fmt.Sprintf("Write error: %+v", writeErr))
    53  			}
    54  		}
    55  
    56  		return err
    57  	})
    58  }
    59  
    60  func discardApiResponse(res *http.Response, okStatus int) error {
    61  	return processApiResponse(res, func(content []byte, statusCode int) error {
    62  		var err error
    63  		if res.StatusCode != okStatus {
    64  			err = fmt.Errorf("command failed with status %d", res.StatusCode)
    65  		}
    66  		return err
    67  	})
    68  }
    69  
    70  func processApiResponse(res *http.Response, doWithContent func(content []byte, statusCode int) error) error {
    71  	var err error
    72  	var responseBytes []byte
    73  
    74  	defer func() {
    75  		if err = res.Body.Close(); err != nil {
    76  			log.Debug(fmt.Sprintf("Error closing response body: %+v", err))
    77  		}
    78  	}()
    79  
    80  	if res.ContentLength > 0 {
    81  		responseBytes, err = io.ReadAll(res.Body)
    82  		if err != nil {
    83  			return err
    84  		}
    85  	} else {
    86  		_, _ = io.Copy(io.Discard, res.Body)
    87  	}
    88  
    89  	if doWithContent == nil {
    90  		return nil
    91  	}
    92  
    93  	return doWithContent(responseBytes, res.StatusCode)
    94  }
    95  
    96  func callWorkerApi(c *components.Context, serverUrl string, serverToken string, method string, body []byte, api ...string) (*http.Response, func(), error) {
    97  	timeout, err := model.GetTimeoutParameter(c)
    98  	if err != nil {
    99  		return nil, nil, err
   100  	}
   101  
   102  	url := fmt.Sprintf("%sworker/api/v1/%s", utils.AddTrailingSlashIfNeeded(serverUrl), strings.Join(api, "/"))
   103  
   104  	reqCtx, cancelReq := context.WithTimeout(context.Background(), timeout)
   105  
   106  	var bodyReader io.Reader
   107  	if body != nil {
   108  		bodyReader = bytes.NewBuffer(body)
   109  	}
   110  
   111  	req, err := http.NewRequestWithContext(reqCtx, method, url, bodyReader)
   112  	if err != nil {
   113  		return nil, cancelReq, err
   114  	}
   115  
   116  	req.Header.Add("Authorization", "Bearer "+strings.TrimSpace(serverToken))
   117  	req.Header.Add("Content-Type", "application/json")
   118  	req.Header.Add("User-Agent", coreutils.GetCliUserAgent())
   119  
   120  	res, err := http.DefaultClient.Do(req)
   121  	if err != nil {
   122  		if errors.Is(err, context.DeadlineExceeded) {
   123  			return nil, cancelReq, fmt.Errorf("request timed out after %s", timeout)
   124  		}
   125  		return nil, cancelReq, err
   126  	}
   127  
   128  	return res, cancelReq, nil
   129  }
   130  
   131  func callWorkerApiWithOutput(c *components.Context, serverUrl string, serverToken string, method string, body []byte, okStatus int, api ...string) error {
   132  	res, discardReq, err := callWorkerApi(c, serverUrl, serverToken, method, body, api...)
   133  	if discardReq != nil {
   134  		defer discardReq()
   135  	}
   136  	if err != nil {
   137  		return err
   138  	}
   139  	return outputApiResponse(res, okStatus)
   140  }
   141  
   142  func callWorkerApiSilent(c *components.Context, serverUrl string, serverToken string, method string, body []byte, okStatus int, api ...string) error {
   143  	res, discardReq, err := callWorkerApi(c, serverUrl, serverToken, method, body, api...)
   144  	if discardReq != nil {
   145  		defer discardReq()
   146  	}
   147  	if err != nil {
   148  		return err
   149  	}
   150  	return discardApiResponse(res, okStatus)
   151  }
   152  
   153  // fetchWorkerDetails Fetch a worker by its name. Returns nil if the worker does not exist (statusCode=404). Any other statusCode other than 200 will result as an error.
   154  func fetchWorkerDetails(c *components.Context, serverUrl string, accessToken string, workerKey string) (*model.WorkerDetails, error) {
   155  	res, discardReq, err := callWorkerApi(c, serverUrl, accessToken, http.MethodGet, nil, "workers", workerKey)
   156  	if discardReq != nil {
   157  		defer discardReq()
   158  	}
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	var details *model.WorkerDetails
   164  
   165  	err = processApiResponse(res, func(content []byte, statusCode int) error {
   166  		if statusCode == http.StatusOK {
   167  			unmarshalled := new(model.WorkerDetails)
   168  			err := json.Unmarshal(content, unmarshalled)
   169  			if err == nil {
   170  				details = unmarshalled
   171  				return nil
   172  			}
   173  			return err
   174  		}
   175  		if statusCode != http.StatusNotFound {
   176  			return fmt.Errorf("fetch worker '%s' failed with status %d", workerKey, statusCode)
   177  		}
   178  		return nil
   179  	})
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	return details, nil
   185  }
   186  
   187  func prepareSecretsUpdate(mf *model.Manifest, existingWorker *model.WorkerDetails) []*model.Secret {
   188  	// We will detect removed secrets
   189  	removedSecrets := map[string]any{}
   190  	if existingWorker != nil {
   191  		for _, existingSecret := range existingWorker.Secrets {
   192  			removedSecrets[existingSecret.Key] = struct{}{}
   193  		}
   194  	}
   195  
   196  	var secrets []*model.Secret
   197  
   198  	// Secrets should have already been decoded
   199  	for secretName, secretValue := range mf.Secrets {
   200  		_, secretExists := removedSecrets[secretName]
   201  		if secretExists {
   202  			// To take into account the local value of a secret
   203  			secrets = append(secrets, &model.Secret{Key: secretName, MarkedForRemoval: true})
   204  		}
   205  		delete(removedSecrets, secretName)
   206  		secrets = append(secrets, &model.Secret{Key: secretName, Value: secretValue})
   207  	}
   208  
   209  	for removedSecret := range removedSecrets {
   210  		secrets = append(secrets, &model.Secret{Key: removedSecret, MarkedForRemoval: true})
   211  	}
   212  
   213  	return secrets
   214  }
   215  
   216  func cleanImports(source string) string {
   217  	out := source
   218  	match := importPattern.FindAllStringSubmatch(out, -1)
   219  	for len(match) == 1 && len(match[0]) == 3 {
   220  		out = match[0][2]
   221  		match = importPattern.FindAllStringSubmatch(out, -1)
   222  	}
   223  	return out
   224  }