618 lines
17 KiB
Go
618 lines
17 KiB
Go
package pkger
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/go-chi/chi/middleware"
|
|
pctx "github.com/influxdata/influxdb/v2/context"
|
|
ierrors "github.com/influxdata/influxdb/v2/kit/errors"
|
|
"github.com/influxdata/influxdb/v2/kit/platform"
|
|
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
|
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
|
"github.com/influxdata/influxdb/v2/pkg/jsonnet"
|
|
"go.uber.org/zap"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const RoutePrefixTemplates = "/api/v2/templates"
|
|
|
|
// HTTPServerTemplates is a server that manages the templates HTTP transport.
|
|
type HTTPServerTemplates struct {
|
|
chi.Router
|
|
api *kithttp.API
|
|
logger *zap.Logger
|
|
svc SVC
|
|
client *http.Client
|
|
}
|
|
|
|
// NewHTTPServerTemplates constructs a new http server.
|
|
func NewHTTPServerTemplates(log *zap.Logger, svc SVC, client *http.Client) *HTTPServerTemplates {
|
|
svr := &HTTPServerTemplates{
|
|
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
|
logger: log,
|
|
svc: svc,
|
|
client: client,
|
|
}
|
|
|
|
exportAllowContentTypes := middleware.AllowContentType("text/yml", "application/x-yaml", "application/json")
|
|
setJSONContentType := middleware.SetHeader("Content-Type", "application/json; charset=utf-8")
|
|
|
|
r := chi.NewRouter()
|
|
{
|
|
r.With(exportAllowContentTypes).Post("/export", svr.export)
|
|
r.With(setJSONContentType).Post("/apply", svr.apply)
|
|
}
|
|
|
|
svr.Router = r
|
|
return svr
|
|
}
|
|
|
|
// Prefix provides the prefix to this route tree.
|
|
func (s *HTTPServerTemplates) Prefix() string {
|
|
return RoutePrefixTemplates
|
|
}
|
|
|
|
// ReqExportOrgIDOpt provides options to export resources by organization id.
|
|
type ReqExportOrgIDOpt struct {
|
|
OrgID string `json:"orgID"`
|
|
Filters struct {
|
|
ByLabel []string `json:"byLabel"`
|
|
ByResourceKind []Kind `json:"byResourceKind"`
|
|
} `json:"resourceFilters"`
|
|
}
|
|
|
|
// ReqExport is a request body for the export endpoint.
|
|
type ReqExport struct {
|
|
StackID string `json:"stackID"`
|
|
OrgIDs []ReqExportOrgIDOpt `json:"orgIDs"`
|
|
Resources []ResourceToClone `json:"resources"`
|
|
}
|
|
|
|
// OK validates a create request.
|
|
func (r *ReqExport) OK() error {
|
|
if len(r.Resources) == 0 && len(r.OrgIDs) == 0 && r.StackID == "" {
|
|
return &errors.Error{
|
|
Code: errors.EUnprocessableEntity,
|
|
Msg: "at least 1 resource, 1 org id, or stack id must be provided",
|
|
}
|
|
}
|
|
|
|
for _, org := range r.OrgIDs {
|
|
if _, err := platform.IDFromString(org.OrgID); err != nil {
|
|
return &errors.Error{
|
|
Code: errors.EInvalid,
|
|
Msg: fmt.Sprintf("provided org id is invalid: %q", org.OrgID),
|
|
}
|
|
}
|
|
}
|
|
|
|
if r.StackID != "" {
|
|
_, err := platform.IDFromString(r.StackID)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RespExport is a response body for the create template endpoint.
|
|
type RespExport []Object
|
|
|
|
func (s *HTTPServerTemplates) export(w http.ResponseWriter, r *http.Request) {
|
|
var reqBody ReqExport
|
|
if err := s.api.DecodeJSON(r.Body, &reqBody); err != nil {
|
|
s.api.Err(w, r, err)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
opts := []ExportOptFn{
|
|
ExportWithExistingResources(reqBody.Resources...),
|
|
}
|
|
for _, orgIDStr := range reqBody.OrgIDs {
|
|
orgID, err := platform.IDFromString(orgIDStr.OrgID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
opts = append(opts, ExportWithAllOrgResources(ExportByOrgIDOpt{
|
|
OrgID: *orgID,
|
|
LabelNames: orgIDStr.Filters.ByLabel,
|
|
ResourceKinds: orgIDStr.Filters.ByResourceKind,
|
|
}))
|
|
}
|
|
|
|
if reqBody.StackID != "" {
|
|
stackID, err := platform.IDFromString(reqBody.StackID)
|
|
if err != nil {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EInvalid,
|
|
Msg: fmt.Sprintf("invalid stack ID provided: %q", reqBody.StackID),
|
|
})
|
|
return
|
|
}
|
|
opts = append(opts, ExportWithStackID(*stackID))
|
|
}
|
|
|
|
newTemplate, err := s.svc.Export(r.Context(), opts...)
|
|
if err != nil {
|
|
s.api.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
resp := RespExport(newTemplate.Objects)
|
|
if resp == nil {
|
|
resp = []Object{}
|
|
}
|
|
|
|
var enc encoder
|
|
switch templateEncoding(r.Header.Get("Accept")) {
|
|
case EncodingYAML:
|
|
enc = yaml.NewEncoder(w)
|
|
w.Header().Set("Content-Type", "application/x-yaml")
|
|
default:
|
|
enc = newJSONEnc(w)
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
}
|
|
|
|
s.encResp(w, r, enc, http.StatusOK, resp)
|
|
}
|
|
|
|
// ReqTemplateRemote provides a package via a remote (i.e. a gist). If content type is not
|
|
// provided then the service will do its best to discern the content type of the
|
|
// contents.
|
|
type ReqTemplateRemote struct {
|
|
URL string `json:"url" yaml:"url"`
|
|
ContentType string `json:"contentType" yaml:"contentType"`
|
|
}
|
|
|
|
// Encoding returns the encoding type that corresponds to the given content type.
|
|
func (p ReqTemplateRemote) Encoding() Encoding {
|
|
return convertEncoding(p.ContentType, p.URL)
|
|
}
|
|
|
|
type ReqRawTemplate struct {
|
|
ContentType string `json:"contentType" yaml:"contentType"`
|
|
Sources []string `json:"sources" yaml:"sources"`
|
|
Template json.RawMessage `json:"contents" yaml:"contents"`
|
|
}
|
|
|
|
func (p ReqRawTemplate) Encoding() Encoding {
|
|
var source string
|
|
if len(p.Sources) > 0 {
|
|
source = p.Sources[0]
|
|
}
|
|
return convertEncoding(p.ContentType, source)
|
|
}
|
|
|
|
// ReqRawAction is a raw action consumers can provide to change the behavior
|
|
// of the application of a template.
|
|
type ReqRawAction struct {
|
|
Action string `json:"action"`
|
|
Properties json.RawMessage `json:"properties"`
|
|
}
|
|
|
|
// ReqApply is the request body for a json or yaml body for the apply template endpoint.
|
|
type ReqApply struct {
|
|
DryRun bool `json:"dryRun" yaml:"dryRun"`
|
|
OrgID string `json:"orgID" yaml:"orgID"`
|
|
StackID *string `json:"stackID" yaml:"stackID"` // optional: non nil value signals stack should be used
|
|
Remotes []ReqTemplateRemote `json:"remotes" yaml:"remotes"`
|
|
|
|
RawTemplates []ReqRawTemplate `json:"templates" yaml:"templates"`
|
|
RawTemplate ReqRawTemplate `json:"template" yaml:"template"`
|
|
|
|
EnvRefs map[string]interface{} `json:"envRefs"`
|
|
Secrets map[string]string `json:"secrets"`
|
|
|
|
RawActions []ReqRawAction `json:"actions"`
|
|
}
|
|
|
|
// Templates returns all templates associated with the request.
|
|
func (r ReqApply) Templates(encoding Encoding, client *http.Client) (*Template, error) {
|
|
var rawTemplates []*Template
|
|
for _, rem := range r.Remotes {
|
|
if rem.URL == "" {
|
|
continue
|
|
}
|
|
template, err := Parse(rem.Encoding(), FromHTTPRequest(rem.URL, client), ValidSkipParseError())
|
|
if err != nil {
|
|
msg := fmt.Sprintf("template from url[%s] had an issue: %s", rem.URL, err.Error())
|
|
return nil, influxErr(errors.EUnprocessableEntity, msg)
|
|
}
|
|
rawTemplates = append(rawTemplates, template)
|
|
}
|
|
|
|
for i, rawTmpl := range append(r.RawTemplates, r.RawTemplate) {
|
|
if rawTmpl.Template == nil {
|
|
continue
|
|
}
|
|
enc := encoding
|
|
if sourceEncoding := rawTmpl.Encoding(); sourceEncoding != EncodingSource {
|
|
enc = sourceEncoding
|
|
}
|
|
template, err := Parse(enc, FromReader(bytes.NewReader(rawTmpl.Template), rawTmpl.Sources...), ValidSkipParseError())
|
|
if err != nil {
|
|
sources := formatSources(rawTmpl.Sources)
|
|
msg := fmt.Sprintf("template[%d] from source(s) %q had an issue: %s", i, sources, err.Error())
|
|
return nil, influxErr(errors.EUnprocessableEntity, msg)
|
|
}
|
|
rawTemplates = append(rawTemplates, template)
|
|
}
|
|
|
|
return Combine(rawTemplates, ValidWithoutResources(), ValidSkipParseError())
|
|
}
|
|
|
|
type actionType string
|
|
|
|
// various ActionTypes the transport API speaks
|
|
const (
|
|
ActionTypeSkipKind actionType = "skipKind"
|
|
ActionTypeSkipResource actionType = "skipResource"
|
|
)
|
|
|
|
func (r ReqApply) validActions() (struct {
|
|
SkipKinds []ActionSkipKind
|
|
SkipResources []ActionSkipResource
|
|
}, error) {
|
|
type actions struct {
|
|
SkipKinds []ActionSkipKind
|
|
SkipResources []ActionSkipResource
|
|
}
|
|
|
|
unmarshalErrFn := func(err error, idx int, actionType string) error {
|
|
msg := fmt.Sprintf("failed to unmarshal properties for actions[%d] %q", idx, actionType)
|
|
return ierrors.Wrap(err, msg)
|
|
}
|
|
|
|
kindErrFn := func(err error, idx int, actionType string) error {
|
|
msg := fmt.Sprintf("invalid kind for actions[%d] %q", idx, actionType)
|
|
return ierrors.Wrap(err, msg)
|
|
}
|
|
|
|
var out actions
|
|
for i, rawAct := range r.RawActions {
|
|
switch a := rawAct.Action; actionType(a) {
|
|
case ActionTypeSkipResource:
|
|
var asr ActionSkipResource
|
|
if err := json.Unmarshal(rawAct.Properties, &asr); err != nil {
|
|
return actions{}, influxErr(errors.EInvalid, unmarshalErrFn(err, i, a))
|
|
}
|
|
if err := asr.Kind.OK(); err != nil {
|
|
return actions{}, influxErr(errors.EInvalid, kindErrFn(err, i, a))
|
|
}
|
|
out.SkipResources = append(out.SkipResources, asr)
|
|
case ActionTypeSkipKind:
|
|
var ask ActionSkipKind
|
|
if err := json.Unmarshal(rawAct.Properties, &ask); err != nil {
|
|
return actions{}, influxErr(errors.EInvalid, unmarshalErrFn(err, i, a))
|
|
}
|
|
if err := ask.Kind.OK(); err != nil {
|
|
return actions{}, influxErr(errors.EInvalid, kindErrFn(err, i, a))
|
|
}
|
|
out.SkipKinds = append(out.SkipKinds, ask)
|
|
default:
|
|
msg := fmt.Sprintf(
|
|
"invalid action type %q provided for actions[%d] ; Must be one of [%s]",
|
|
a, i, ActionTypeSkipResource,
|
|
)
|
|
return actions{}, influxErr(errors.EInvalid, msg)
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// RespApply is the response body for the apply template endpoint.
|
|
type RespApply struct {
|
|
Sources []string `json:"sources" yaml:"sources"`
|
|
StackID string `json:"stackID" yaml:"stackID"`
|
|
Diff Diff `json:"diff" yaml:"diff"`
|
|
Summary Summary `json:"summary" yaml:"summary"`
|
|
|
|
Errors []ValidationErr `json:"errors,omitempty" yaml:"errors,omitempty"`
|
|
}
|
|
|
|
// RespApplyErr is the response body for a dry-run parse error in the apply template endpoint.
|
|
type RespApplyErr struct {
|
|
RespApply
|
|
|
|
Code string `json:"code" yaml:"code"`
|
|
Message string `json:"message" yaml:"message"`
|
|
}
|
|
|
|
func (s *HTTPServerTemplates) apply(w http.ResponseWriter, r *http.Request) {
|
|
var reqBody ReqApply
|
|
encoding, err := decodeWithEncoding(r, &reqBody)
|
|
if err != nil {
|
|
s.api.Err(w, r, newDecodeErr(encoding.String(), err))
|
|
return
|
|
}
|
|
|
|
orgID, err := platform.IDFromString(reqBody.OrgID)
|
|
if err != nil {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EInvalid,
|
|
Msg: fmt.Sprintf("invalid organization ID provided: %q", reqBody.OrgID),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Reject use of server-side jsonnet with /api/v2/templates/apply
|
|
if encoding == EncodingJsonnet {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EUnprocessableEntity,
|
|
Msg: fmt.Sprintf("template from source(s) had an issue: %s", ErrInvalidEncoding.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
var remotes []string
|
|
for _, rem := range reqBody.Remotes {
|
|
remotes = append(remotes, rem.URL)
|
|
}
|
|
remotes = append(remotes, reqBody.RawTemplate.Sources...)
|
|
|
|
for _, rem := range remotes {
|
|
// While things like '.%6Aonnet' evaluate to the default encoding (yaml), let's unescape and catch those too
|
|
decoded, err := url.QueryUnescape(rem)
|
|
if err != nil {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EInvalid,
|
|
Msg: fmt.Sprintf("template from url[%q] had an issue", rem),
|
|
})
|
|
return
|
|
}
|
|
if len(decoded) > 0 && strings.HasSuffix(strings.ToLower(decoded), "jsonnet") {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EUnprocessableEntity,
|
|
Msg: fmt.Sprintf("template from url[%q] had an issue: %s", rem, ErrInvalidEncoding.Error()),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
var stackID platform.ID
|
|
if reqBody.StackID != nil {
|
|
if err := stackID.DecodeFromString(*reqBody.StackID); err != nil {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EInvalid,
|
|
Msg: fmt.Sprintf("invalid stack ID provided: %q", *reqBody.StackID),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
parsedTemplate, err := reqBody.Templates(encoding, s.client)
|
|
if err != nil {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Code: errors.EUnprocessableEntity,
|
|
Err: err,
|
|
})
|
|
return
|
|
}
|
|
|
|
actions, err := reqBody.validActions()
|
|
if err != nil {
|
|
s.api.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
applyOpts := []ApplyOptFn{
|
|
ApplyWithEnvRefs(reqBody.EnvRefs),
|
|
ApplyWithTemplate(parsedTemplate),
|
|
ApplyWithStackID(stackID),
|
|
}
|
|
for _, a := range actions.SkipResources {
|
|
applyOpts = append(applyOpts, ApplyWithResourceSkip(a))
|
|
}
|
|
for _, a := range actions.SkipKinds {
|
|
applyOpts = append(applyOpts, ApplyWithKindSkip(a))
|
|
}
|
|
|
|
auth, err := pctx.GetAuthorizer(r.Context())
|
|
if err != nil {
|
|
s.api.Err(w, r, err)
|
|
return
|
|
}
|
|
userID := auth.GetUserID()
|
|
|
|
if reqBody.DryRun {
|
|
impact, err := s.svc.DryRun(r.Context(), *orgID, userID, applyOpts...)
|
|
if IsParseErr(err) {
|
|
s.api.Respond(w, r, http.StatusUnprocessableEntity, RespApplyErr{
|
|
RespApply: impactToRespApply(impact, err),
|
|
Code: errors.EUnprocessableEntity,
|
|
Message: "unprocessable entity",
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
s.api.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
s.api.Respond(w, r, http.StatusOK, impactToRespApply(impact, nil))
|
|
return
|
|
}
|
|
|
|
applyOpts = append(applyOpts, ApplyWithSecrets(reqBody.Secrets))
|
|
|
|
impact, err := s.svc.Apply(r.Context(), *orgID, userID, applyOpts...)
|
|
if err != nil && !IsParseErr(err) {
|
|
s.api.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
s.api.Respond(w, r, http.StatusCreated, impactToRespApply(impact, err))
|
|
}
|
|
|
|
func (s *HTTPServerTemplates) encResp(w http.ResponseWriter, r *http.Request, enc encoder, code int, res interface{}) {
|
|
w.WriteHeader(code)
|
|
if err := enc.Encode(res); err != nil {
|
|
s.api.Err(w, r, &errors.Error{
|
|
Msg: fmt.Sprintf("unable to marshal; Err: %v", err),
|
|
Code: errors.EInternal,
|
|
Err: err,
|
|
})
|
|
}
|
|
}
|
|
|
|
func impactToRespApply(impact ImpactSummary, err error) RespApply {
|
|
out := RespApply{
|
|
Sources: append([]string{}, impact.Sources...), // guarantee non nil slice
|
|
StackID: impact.StackID.String(),
|
|
Diff: impact.Diff,
|
|
Summary: impact.Summary,
|
|
}
|
|
if err != nil {
|
|
out.Errors = convertParseErr(err)
|
|
}
|
|
if out.Diff.Buckets == nil {
|
|
out.Diff.Buckets = []DiffBucket{}
|
|
}
|
|
if out.Diff.Checks == nil {
|
|
out.Diff.Checks = []DiffCheck{}
|
|
}
|
|
if out.Diff.Dashboards == nil {
|
|
out.Diff.Dashboards = []DiffDashboard{}
|
|
}
|
|
if out.Diff.Labels == nil {
|
|
out.Diff.Labels = []DiffLabel{}
|
|
}
|
|
if out.Diff.LabelMappings == nil {
|
|
out.Diff.LabelMappings = []DiffLabelMapping{}
|
|
}
|
|
if out.Diff.NotificationEndpoints == nil {
|
|
out.Diff.NotificationEndpoints = []DiffNotificationEndpoint{}
|
|
}
|
|
if out.Diff.NotificationRules == nil {
|
|
out.Diff.NotificationRules = []DiffNotificationRule{}
|
|
}
|
|
if out.Diff.NotificationRules == nil {
|
|
out.Diff.NotificationRules = []DiffNotificationRule{}
|
|
}
|
|
if out.Diff.Tasks == nil {
|
|
out.Diff.Tasks = []DiffTask{}
|
|
}
|
|
if out.Diff.Telegrafs == nil {
|
|
out.Diff.Telegrafs = []DiffTelegraf{}
|
|
}
|
|
if out.Diff.Variables == nil {
|
|
out.Diff.Variables = []DiffVariable{}
|
|
}
|
|
|
|
if out.Summary.Buckets == nil {
|
|
out.Summary.Buckets = []SummaryBucket{}
|
|
}
|
|
if out.Summary.Checks == nil {
|
|
out.Summary.Checks = []SummaryCheck{}
|
|
}
|
|
if out.Summary.Dashboards == nil {
|
|
out.Summary.Dashboards = []SummaryDashboard{}
|
|
}
|
|
if out.Summary.Labels == nil {
|
|
out.Summary.Labels = []SummaryLabel{}
|
|
}
|
|
if out.Summary.LabelMappings == nil {
|
|
out.Summary.LabelMappings = []SummaryLabelMapping{}
|
|
}
|
|
if out.Summary.NotificationEndpoints == nil {
|
|
out.Summary.NotificationEndpoints = []SummaryNotificationEndpoint{}
|
|
}
|
|
if out.Summary.NotificationRules == nil {
|
|
out.Summary.NotificationRules = []SummaryNotificationRule{}
|
|
}
|
|
if out.Summary.NotificationRules == nil {
|
|
out.Summary.NotificationRules = []SummaryNotificationRule{}
|
|
}
|
|
if out.Summary.Tasks == nil {
|
|
out.Summary.Tasks = []SummaryTask{}
|
|
}
|
|
if out.Summary.TelegrafConfigs == nil {
|
|
out.Summary.TelegrafConfigs = []SummaryTelegraf{}
|
|
}
|
|
if out.Summary.Variables == nil {
|
|
out.Summary.Variables = []SummaryVariable{}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func formatSources(sources []string) string {
|
|
return strings.Join(sources, "; ")
|
|
}
|
|
|
|
func decodeWithEncoding(r *http.Request, v interface{}) (Encoding, error) {
|
|
encoding := templateEncoding(r.Header.Get("Content-Type"))
|
|
|
|
var dec interface{ Decode(interface{}) error }
|
|
switch encoding {
|
|
case EncodingJsonnet:
|
|
dec = jsonnet.NewDecoder(r.Body)
|
|
case EncodingYAML:
|
|
dec = yaml.NewDecoder(r.Body)
|
|
default:
|
|
dec = json.NewDecoder(r.Body)
|
|
}
|
|
|
|
return encoding, dec.Decode(v)
|
|
}
|
|
|
|
func templateEncoding(contentType string) Encoding {
|
|
switch contentType {
|
|
case "application/x-jsonnet":
|
|
return EncodingJsonnet
|
|
case "text/yml", "application/x-yaml":
|
|
return EncodingYAML
|
|
default:
|
|
return EncodingJSON
|
|
}
|
|
}
|
|
|
|
func convertEncoding(ct, rawURL string) Encoding {
|
|
ct = strings.ToLower(ct)
|
|
urlBase := path.Ext(rawURL)
|
|
switch {
|
|
case ct == "jsonnet" || urlBase == ".jsonnet":
|
|
return EncodingJsonnet
|
|
case ct == "json" || urlBase == ".json":
|
|
return EncodingJSON
|
|
case ct == "yml" || ct == "yaml" || urlBase == ".yml" || urlBase == ".yaml":
|
|
return EncodingYAML
|
|
default:
|
|
return EncodingSource
|
|
}
|
|
}
|
|
|
|
type encoder interface {
|
|
Encode(interface{}) error
|
|
}
|
|
|
|
func newJSONEnc(w io.Writer) encoder {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", "\t")
|
|
return enc
|
|
}
|
|
|
|
func convertParseErr(err error) []ValidationErr {
|
|
pErr, ok := err.(ParseError)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return pErr.ValidationErrs()
|
|
}
|
|
|
|
func newDecodeErr(encoding string, err error) *errors.Error {
|
|
return &errors.Error{
|
|
Msg: fmt.Sprintf("unable to unmarshal %s", encoding),
|
|
Code: errors.EInvalid,
|
|
Err: err,
|
|
}
|
|
}
|