GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.go 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package client
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io/ioutil"
  8. "net/http"
  9. "strings"
  10. "github.com/gorilla/mux"
  11. "github.com/pkg/errors"
  12. "github.com/fluxcd/flux/api"
  13. "github.com/fluxcd/flux/api/v10"
  14. "github.com/fluxcd/flux/api/v11"
  15. "github.com/fluxcd/flux/api/v6"
  16. "github.com/fluxcd/flux/api/v9"
  17. fluxerr "github.com/fluxcd/flux/errors"
  18. "github.com/fluxcd/flux/event"
  19. transport "github.com/fluxcd/flux/http"
  20. "github.com/fluxcd/flux/job"
  21. "github.com/fluxcd/flux/update"
  22. )
  23. var (
  24. errNotImplemented = errors.New("not implemented")
  25. )
  26. type Token string
  27. func (t Token) Set(req *http.Request) {
  28. if string(t) != "" {
  29. req.Header.Set("Authorization", fmt.Sprintf("Scope-Probe token=%s", t))
  30. }
  31. }
  32. type Client struct {
  33. client *http.Client
  34. token Token
  35. router *mux.Router
  36. endpoint string
  37. }
  38. var _ api.Server = &Client{}
  39. func New(c *http.Client, router *mux.Router, endpoint string, t Token) *Client {
  40. return &Client{
  41. client: c,
  42. token: t,
  43. router: router,
  44. endpoint: endpoint,
  45. }
  46. }
  47. func (c *Client) Ping(ctx context.Context) error {
  48. return c.Get(ctx, nil, transport.Ping)
  49. }
  50. func (c *Client) Version(ctx context.Context) (string, error) {
  51. var v string
  52. err := c.Get(ctx, &v, transport.Version)
  53. return v, err
  54. }
  55. func (c *Client) NotifyChange(ctx context.Context, change v9.Change) error {
  56. return c.PostWithBody(ctx, transport.Notify, change)
  57. }
  58. func (c *Client) ListServices(ctx context.Context, namespace string) ([]v6.ControllerStatus, error) {
  59. var res []v6.ControllerStatus
  60. err := c.Get(ctx, &res, transport.ListServices, "namespace", namespace)
  61. return res, err
  62. }
  63. func (c *Client) ListServicesWithOptions(ctx context.Context, opts v11.ListServicesOptions) ([]v6.ControllerStatus, error) {
  64. var res []v6.ControllerStatus
  65. var services []string
  66. for _, svc := range opts.Services {
  67. services = append(services, svc.String())
  68. }
  69. err := c.Get(ctx, &res, transport.ListServicesWithOptions, "namespace", opts.Namespace, "services", strings.Join(services, ","))
  70. return res, err
  71. }
  72. func (c *Client) ListImages(ctx context.Context, s update.ResourceSpec) ([]v6.ImageStatus, error) {
  73. var res []v6.ImageStatus
  74. err := c.Get(ctx, &res, transport.ListImages, "service", string(s))
  75. return res, err
  76. }
  77. func (c *Client) ListImagesWithOptions(ctx context.Context, opts v10.ListImagesOptions) ([]v6.ImageStatus, error) {
  78. var res []v6.ImageStatus
  79. err := c.Get(ctx, &res, transport.ListImagesWithOptions, "service", string(opts.Spec), "containerFields", strings.Join(opts.OverrideContainerFields, ","), "namespace", opts.Namespace)
  80. return res, err
  81. }
  82. func (c *Client) JobStatus(ctx context.Context, jobID job.ID) (job.Status, error) {
  83. var res job.Status
  84. err := c.Get(ctx, &res, transport.JobStatus, "id", string(jobID))
  85. return res, err
  86. }
  87. func (c *Client) SyncStatus(ctx context.Context, ref string) ([]string, error) {
  88. var res []string
  89. err := c.Get(ctx, &res, transport.SyncStatus, "ref", ref)
  90. return res, err
  91. }
  92. func (c *Client) UpdateManifests(ctx context.Context, spec update.Spec) (job.ID, error) {
  93. var res job.ID
  94. err := c.methodWithResp(ctx, "POST", &res, transport.UpdateManifests, spec)
  95. return res, err
  96. }
  97. func (c *Client) LogEvent(ctx context.Context, event event.Event) error {
  98. return c.PostWithBody(ctx, transport.LogEvent, event)
  99. }
  100. func (c *Client) Export(ctx context.Context) ([]byte, error) {
  101. var res []byte
  102. err := c.Get(ctx, &res, transport.Export)
  103. return res, err
  104. }
  105. func (c *Client) GitRepoConfig(ctx context.Context, regenerate bool) (v6.GitConfig, error) {
  106. var res v6.GitConfig
  107. err := c.methodWithResp(ctx, "POST", &res, transport.GitRepoConfig, regenerate)
  108. return res, err
  109. }
  110. // --- Request helpers
  111. // Post is a simple query-param only post request
  112. func (c *Client) Post(ctx context.Context, route string, queryParams ...string) error {
  113. return c.PostWithBody(ctx, route, nil, queryParams...)
  114. }
  115. // PostWithBody is a more complex post request, which includes a json-ified body.
  116. // If body is not nil, it is encoded to json before sending
  117. func (c *Client) PostWithBody(ctx context.Context, route string, body interface{}, queryParams ...string) error {
  118. return c.methodWithResp(ctx, "POST", nil, route, body, queryParams...)
  119. }
  120. func (c *Client) PatchWithBody(ctx context.Context, route string, body interface{}, queryParams ...string) error {
  121. return c.methodWithResp(ctx, "PATCH", nil, route, body, queryParams...)
  122. }
  123. // methodWithResp is the full enchilada, it handles body and query-param
  124. // encoding, as well as decoding the response into the provided destination.
  125. // Note, the response will only be decoded into the dest if the len is > 0.
  126. func (c *Client) methodWithResp(ctx context.Context, method string, dest interface{}, route string, body interface{}, queryParams ...string) error {
  127. u, err := transport.MakeURL(c.endpoint, c.router, route, queryParams...)
  128. if err != nil {
  129. return errors.Wrap(err, "constructing URL")
  130. }
  131. var bodyBytes []byte
  132. if body != nil {
  133. bodyBytes, err = json.Marshal(body)
  134. if err != nil {
  135. return errors.Wrap(err, "encoding request body")
  136. }
  137. }
  138. req, err := http.NewRequest(method, u.String(), bytes.NewReader(bodyBytes))
  139. if err != nil {
  140. return errors.Wrapf(err, "constructing request %s", u)
  141. }
  142. req = req.WithContext(ctx)
  143. c.token.Set(req)
  144. req.Header.Set("Accept", "application/json")
  145. resp, err := c.executeRequest(req)
  146. if err != nil {
  147. return errors.Wrap(err, "executing HTTP request")
  148. }
  149. defer resp.Body.Close()
  150. respBytes, err := ioutil.ReadAll(resp.Body)
  151. if err != nil {
  152. return errors.Wrap(err, "decoding response from server")
  153. }
  154. if len(respBytes) <= 0 {
  155. return nil
  156. }
  157. if err := json.Unmarshal(respBytes, &dest); err != nil {
  158. return errors.Wrap(err, "decoding response from server")
  159. }
  160. return nil
  161. }
  162. // Get executes a get request against the Flux server. it unmarshals the response into dest, if not nil.
  163. func (c *Client) Get(ctx context.Context, dest interface{}, route string, queryParams ...string) error {
  164. u, err := transport.MakeURL(c.endpoint, c.router, route, queryParams...)
  165. if err != nil {
  166. return errors.Wrap(err, "constructing URL")
  167. }
  168. req, err := http.NewRequest("GET", u.String(), nil)
  169. if err != nil {
  170. return errors.Wrapf(err, "constructing request %s", u)
  171. }
  172. req = req.WithContext(ctx)
  173. c.token.Set(req)
  174. req.Header.Set("Accept", "application/json")
  175. resp, err := c.executeRequest(req)
  176. if err != nil {
  177. return errors.Wrap(err, "executing HTTP request")
  178. }
  179. defer resp.Body.Close()
  180. if dest != nil {
  181. if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
  182. return errors.Wrap(err, "decoding response from server")
  183. }
  184. }
  185. return nil
  186. }
  187. func (c *Client) executeRequest(req *http.Request) (*http.Response, error) {
  188. resp, err := c.client.Do(req)
  189. if err != nil {
  190. return nil, errors.Wrap(err, "executing HTTP request")
  191. }
  192. switch resp.StatusCode {
  193. case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted:
  194. return resp, nil
  195. case http.StatusUnauthorized:
  196. return resp, transport.ErrorUnauthorized
  197. default:
  198. body, err := ioutil.ReadAll(resp.Body)
  199. if err != nil {
  200. return resp, errors.Wrap(err, "reading response body of error")
  201. }
  202. // Use the content type to discriminate between `fluxerr.Error`,
  203. // and the previous "any old error"
  204. if strings.HasPrefix(resp.Header.Get(http.CanonicalHeaderKey("Content-Type")), "application/json") {
  205. var niceError fluxerr.Error
  206. if err := json.Unmarshal(body, &niceError); err != nil {
  207. return resp, errors.Wrap(err, "decoding response body of error")
  208. }
  209. // just in case it's JSON but not one of our own errors
  210. if niceError.Err != nil {
  211. return resp, &niceError
  212. }
  213. // fallthrough
  214. }
  215. return resp, errors.New(resp.Status + " " + string(body))
  216. }
  217. }