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 }