Skip to content

Commit

Permalink
CHYT-1010: add json format for controller HTTP API
Browse files Browse the repository at this point in the history
  • Loading branch information
chizhonkova committed Oct 10, 2023
1 parent ebc4b6a commit bc6f550
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 50 deletions.
8 changes: 4 additions & 4 deletions yt/chyt/controller/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ func (a *API) Status(ctx context.Context, alias string) (strawberry.OpletStatus,
return oplet.Status()
}

func (a *API) GetOption(ctx context.Context, alias, key string) (value yson.RawValue, err error) {
func (a *API) GetOption(ctx context.Context, alias, key string) (value any, err error) {
if err = a.CheckExistence(ctx, alias, true /*shouldExist*/); err != nil {
return
}
Expand Down Expand Up @@ -405,8 +405,8 @@ func (a *API) RemoveOption(ctx context.Context, alias, key string) error {
}

type AliasWithAttrs struct {
Alias string `yson:",value"`
Attrs map[string]any `yson:",attrs"`
Alias string `yson:",value" json:"$value"`
Attrs map[string]any `yson:",attrs" json:"$attributes"`
}

func (a *API) List(ctx context.Context, attributes []string) ([]AliasWithAttrs, error) {
Expand Down Expand Up @@ -483,7 +483,7 @@ func (a *API) List(ctx context.Context, attributes []string) ([]AliasWithAttrs,
return result, nil
}

func (a *API) GetSpeclet(ctx context.Context, alias string) (speclet yson.RawValue, err error) {
func (a *API) GetSpeclet(ctx context.Context, alias string) (speclet map[string]any, err error) {
if err = a.CheckExistence(ctx, alias, true /*shouldExist*/); err != nil {
return
}
Expand Down
91 changes: 77 additions & 14 deletions yt/chyt/controller/internal/api/http.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"encoding/json"
"fmt"
"io"
"net/http"
Expand All @@ -17,7 +18,7 @@ import (
type RequestParams struct {
// Params contains request parameters which are set by the user.
// E.g. in CLI "--xxx yyy" should set an "xxx" parameter with the value "yyy".
Params map[string]any `yson:"params"`
Params map[string]any `yson:"params" json:"params"`

// Unparsed indicates that:
//
Expand All @@ -27,7 +28,48 @@ type RequestParams struct {
// 3. A null value for a param is equivalent to a missing param.
//
// It can be useful in CLI, where all params' types are unknown.
Unparsed bool `yson:"unparsed"`
Unparsed bool `yson:"unparsed" json:"unparsed"`
}

type FormatType string

const (
FormatJSON FormatType = "json"
FormatYSON FormatType = "yson"

DefaultFormat FormatType = FormatYSON
)

func getFormat(formatHeader string) (FormatType, error) {
switch formatHeader {
case "application/yson":
return FormatYSON, nil
case "application/json":
return FormatJSON, nil
case "", "*/*":
return DefaultFormat, nil
}
return "", yterrors.Err("cannot get format, invalid header", yterrors.Attr("header", formatHeader))
}

func Unmarshal(data []byte, v any, format FormatType) error {
switch format {
case FormatYSON:
return yson.Unmarshal(data, v)
case FormatJSON:
return json.Unmarshal(data, v)
}
return yterrors.Err("cannot unmarshal, invalid format type")
}

func Marshal(v any, format FormatType) ([]byte, error) {
switch format {
case FormatYSON:
return yson.Marshal(v)
case FormatJSON:
return json.Marshal(v)
}
return nil, yterrors.Err("cannot marshal, invalid format type")
}

// HTTPAPI is a lightweight wrapper of API which handles http requests and transforms them to proper API calls.
Expand All @@ -46,13 +88,20 @@ func NewHTTPAPI(ytc yt.Client, config APIConfig, ctl strawberry.Controller, l lo
}

func (a HTTPAPI) reply(w http.ResponseWriter, status int, rsp any) {
body, err := yson.Marshal(rsp)
format, err := getFormat(w.Header().Get("Content-Type"))
if err != nil {
a.l.Error("failed to get output format", log.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}

body, err := Marshal(rsp, format)
if err != nil {
a.l.Error("failed to marshal response", log.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header()["Content-Type"] = []string{"application/yson"}

w.WriteHeader(status)
_, err = w.Write(body)
if err != nil {
Expand Down Expand Up @@ -123,23 +172,39 @@ func (c CmdParameter) AsExplicit() CmdParameter {
return c
}

type HandlerFunc func(api HTTPAPI, w http.ResponseWriter, r *http.Request, params map[string]any)

type CmdDescriptor struct {
Name string `yson:"name"`
Parameters []CmdParameter `yson:"parameters"`
Description string `yson:"description,omitempty"`
Handler func(api HTTPAPI, w http.ResponseWriter, r *http.Request, params map[string]any) `yson:"-"`
Name string `yson:"name"`
Parameters []CmdParameter `yson:"parameters"`
Description string `yson:"description,omitempty"`
Handler HandlerFunc `yson:"-"`
}

func (a HTTPAPI) parseAndValidateRequestParams(w http.ResponseWriter, r *http.Request, cmd CmdDescriptor) map[string]any {
outputFormat, err := getFormat(r.Header.Get("Accept"))
if err != nil {
a.replyWithError(w, err)
return nil
}

// We need to set this header immediately because all reply functions use it.
w.Header()["Content-Type"] = []string{fmt.Sprintf("application/%v", outputFormat)}

inputFormat, err := getFormat(r.Header.Get("Content-Type"))
if err != nil {
a.replyWithError(w, err)
return nil
}

body, err := io.ReadAll(r.Body)
if err != nil {
a.replyWithError(w, yterrors.Err("error reading request body", err))
return nil
}

var request RequestParams
err = yson.Unmarshal(body, &request)
if err != nil {
if err = Unmarshal(body, &request, inputFormat); err != nil {
a.replyWithError(w, yterrors.Err("error parsing request body", err))
return nil
}
Expand Down Expand Up @@ -183,8 +248,7 @@ func (a HTTPAPI) parseAndValidateRequestParams(w http.ResponseWriter, r *http.Re
// Try to parse anything except the TypeString as a yson-string.
if param.Type != TypeString {
var parsedValue any
err := yson.Unmarshal([]byte(unparsedValue), &parsedValue)
if err != nil {
if err = Unmarshal([]byte(unparsedValue), &parsedValue, inputFormat); err != nil {
a.replyWithError(w, err)
return nil
}
Expand All @@ -202,8 +266,7 @@ func (a HTTPAPI) parseAndValidateRequestParams(w http.ResponseWriter, r *http.Re
}
if param.ElementType != TypeString {
var parsedElement any
err := yson.Unmarshal([]byte(unparsedElement), &parsedElement)
if err != nil {
if err := Unmarshal([]byte(unparsedElement), &parsedElement, inputFormat); err != nil {
a.replyWithError(w, err)
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions yt/chyt/controller/internal/strawberry/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ func extractOpID(err error) yt.OperationID {
}

type FieldDiff struct {
OldValue any `yson:"old_value"`
NewValue any `yson:"new_value"`
OldValue any `yson:"old_value" json:"old_value"`
NewValue any `yson:"new_value" json:"new_value"`
}

func specletDiff(oldSpeclet, newSpeclet any) map[string]FieldDiff {
Expand Down
28 changes: 14 additions & 14 deletions yt/chyt/controller/internal/strawberry/oplet.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,20 +863,20 @@ func (oplet *Oplet) Pass(ctx context.Context) error {
}

type OpletStatus struct {
Status string `yson:"status"`
SpecletDiff map[string]FieldDiff `yson:"speclet_diff,omitempty"`
OperationURL string `yson:"operation_url,omitempty"`
OperationState yt.OperationState `yson:"yt_operation_state,omitempty"`
OperationID yt.OperationID `yson:"operation_id,omitempty"`
State OperationState `yson:"state"`
Creator string `yson:"creator,omitempty"`
Pool string `yson:"pool,omitempty"`
Stage string `yson:"stage"`
StartTime yson.Time `yson:"start_time,omitempty"`
FinishTime yson.Time `yson:"finish_time,omitempty"`
IncarnationIndex int `yson:"incarnation_index"`
CtlAttributes map[string]any `yson:"ctl_attributes"`
Error string `yson:"error,omitempty"`
Status string `yson:"status" json:"status"`
SpecletDiff map[string]FieldDiff `yson:"speclet_diff,omitempty" json:"speclet_diff,omitempty"`
OperationURL string `yson:"operation_url,omitempty" json:"operation_url,omitempty"`
OperationState yt.OperationState `yson:"yt_operation_state,omitempty" json:"yt_operation_state,omitempty"`
OperationID yt.OperationID `yson:"operation_id,omitempty" json:"operation_id,omitempty"`
State OperationState `yson:"state" json:"state"`
Creator string `yson:"creator,omitempty" json:"creator,omitempty"`
Pool string `yson:"pool,omitempty" json:"pool,omitempty"`
Stage string `yson:"stage" json:"stage"`
StartTime yson.Time `yson:"start_time,omitempty" json:"start_time,omitempty"`
FinishTime yson.Time `yson:"finish_time,omitempty" json:"finish_time,omitempty"`
IncarnationIndex int `yson:"incarnation_index" json:"incarnation_index"`
CtlAttributes map[string]any `yson:"ctl_attributes" json:"ctl_attributes"`
Error string `yson:"error,omitempty" json:"error,omitempty"`
}

func (oplet *Oplet) Status() (s OpletStatus, err error) {
Expand Down
18 changes: 9 additions & 9 deletions yt/chyt/controller/internal/strawberry/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ const (
)

type OptionDescriptor struct {
Name string `yson:"name"`
Type OptionType `yson:"type"`
CurrentValue any `yson:"current_value,omitempty"`
DefaultValue any `yson:"default_value,omitempty"`
Choises []any `yson:"choises,omitempty"`
Description string `yson:"description,omitempty"`
Name string `yson:"name" json:"name"`
Type OptionType `yson:"type" json:"type"`
CurrentValue any `yson:"current_value,omitempty" json:"current_value,omitempty"`
DefaultValue any `yson:"default_value,omitempty" json:"default_value,omitempty"`
Choises []any `yson:"choises,omitempty" json:"choises,omitempty"`
Description string `yson:"description,omitempty" json:"description,omitempty"`
}

type OptionGroupDescriptor struct {
Title string `yson:"title"`
Options []OptionDescriptor `yson:"options"`
Title string `yson:"title" json:"title"`
Options []OptionDescriptor `yson:"options" json:"options"`

// Hidden indicates that the option group consists of non-important or rarely used options
// and these options should be hidden in UI if possible (e.g. under a cut element).
Hidden bool `yson:"hidden"`
Hidden bool `yson:"hidden" json:"hidden"`
}
20 changes: 13 additions & 7 deletions yt/chyt/controller/test/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package helpers

import (
"bytes"
"fmt"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -68,19 +69,20 @@ type RequestClient struct {

type Response struct {
StatusCode int
Body yson.RawValue
Body []byte
}

func (c *RequestClient) MakeRequest(httpMethod string, command string, params api.RequestParams) Response {
body, err := yson.Marshal(params)
func (c *RequestClient) MakeRequest(httpMethod string, command string, params api.RequestParams, format api.FormatType) Response {
body, err := api.Marshal(params, format)
require.NoError(c.t, err)

c.env.L.Debug("making http api request", log.String("command", command), log.Any("params", params))

req, err := http.NewRequest(httpMethod, c.Endpoint+"/"+command, bytes.NewReader(body))
require.NoError(c.t, err)

req.Header.Set("Content-Type", "application/yson")
req.Header.Set("Content-Type", fmt.Sprintf("application/%v", format))
req.Header.Set("Accept", fmt.Sprintf("application/%v", format))
req.Header.Set("X-YT-TestUser", c.User)

rsp, err := c.httpClient.Do(req)
Expand All @@ -97,16 +99,20 @@ func (c *RequestClient) MakeRequest(httpMethod string, command string, params ap

return Response{
StatusCode: rsp.StatusCode,
Body: yson.RawValue(body),
Body: body,
}
}

func (c *RequestClient) MakePostRequest(command string, params api.RequestParams) Response {
return c.MakeRequest(http.MethodPost, c.Proxy+"/"+command, params)
return c.MakeRequest(http.MethodPost, c.Proxy+"/"+command, params, api.DefaultFormat)
}

func (c *RequestClient) MakePostRequestWithFormat(command string, params api.RequestParams, format api.FormatType) Response {
return c.MakeRequest(http.MethodPost, c.Proxy+"/"+command, params, format)
}

func (c *RequestClient) MakeGetRequest(command string, params api.RequestParams) Response {
return c.MakeRequest(http.MethodGet, command, params)
return c.MakeRequest(http.MethodGet, command, params, api.DefaultFormat)
}

func PrepareClient(t *testing.T, env *Env, proxy string, server *httpserver.HTTPServer) *RequestClient {
Expand Down
36 changes: 36 additions & 0 deletions yt/chyt/controller/test/integration/api_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package integration

import (
"encoding/json"
"io"
"net/http"
"reflect"
Expand Down Expand Up @@ -737,3 +738,38 @@ func TestHTTPAPIDescribeOptions(t *testing.T) {
CurrentValue: uint64(10),
})
}

func TestHTTPAPIJSONFormat(t *testing.T) {
t.Parallel()

_, c := helpers.PrepareAPI(t)
alias := helpers.GenerateAlias()

r := c.MakePostRequestWithFormat(
"create",
api.RequestParams{Params: map[string]any{"alias": alias}},
"json")
require.Equal(t, http.StatusOK, r.StatusCode)

r = c.MakePostRequestWithFormat(
"list",
api.RequestParams{
Params: map[string]any{
"attributes": []string{"creator", "test_option"},
},
},
"json",
)
require.Equal(t, http.StatusOK, r.StatusCode)

var resultWithAttrs map[string][]api.AliasWithAttrs
require.NoError(t, json.Unmarshal(r.Body, &resultWithAttrs))

require.Contains(t, resultWithAttrs["result"], api.AliasWithAttrs{
Alias: alias,
Attrs: map[string]any{
"creator": "root",
"test_option": nil,
},
})
}

0 comments on commit bc6f550

Please sign in to comment.