parent
9bd5de23bd
commit
2927f0e718
|
@ -1019,10 +1019,22 @@ func (m *Launcher) run(ctx context.Context) (err error) {
|
|||
pkgSVC = pkger.MWAuth(authAgent)(pkgSVC)
|
||||
}
|
||||
|
||||
var pkgHTTPServer *pkger.HTTPServerPackages
|
||||
var pkgHTTPServerDeprecated *pkger.HTTPServerPackages
|
||||
{
|
||||
pkgServerLogger := m.log.With(zap.String("handler", "pkger"))
|
||||
pkgHTTPServer = pkger.NewHTTPServerPackages(pkgServerLogger, pkgSVC)
|
||||
pkgHTTPServerDeprecated = pkger.NewHTTPServerPackages(pkgServerLogger, pkgSVC)
|
||||
}
|
||||
|
||||
var stacksHTTPServer *pkger.HTTPServerStacks
|
||||
{
|
||||
tLogger := m.log.With(zap.String("handler", "stacks"))
|
||||
stacksHTTPServer = pkger.NewHTTPServerStacks(tLogger, pkgSVC)
|
||||
}
|
||||
|
||||
var templatesHTTPServer *pkger.HTTPServerTemplates
|
||||
{
|
||||
tLogger := m.log.With(zap.String("handler", "templates"))
|
||||
templatesHTTPServer = pkger.NewHTTPServerTemplates(tLogger, pkgSVC)
|
||||
}
|
||||
|
||||
var userHTTPServer *tenant.UserHandler
|
||||
|
@ -1093,7 +1105,9 @@ func (m *Launcher) run(ctx context.Context) (err error) {
|
|||
|
||||
{
|
||||
platformHandler := http.NewPlatformHandler(m.apibackend,
|
||||
http.WithResourceHandler(pkgHTTPServer),
|
||||
http.WithResourceHandler(pkgHTTPServerDeprecated),
|
||||
http.WithResourceHandler(stacksHTTPServer),
|
||||
http.WithResourceHandler(templatesHTTPServer),
|
||||
http.WithResourceHandler(onboardHTTPServer),
|
||||
http.WithResourceHandler(authHTTPServer),
|
||||
http.WithResourceHandler(kithttp.NewFeatureHandler(feature.NewLabelPackage(), m.flagger, oldLabelHandler, labelHandler, labelHandler.Prefix())),
|
||||
|
|
270
http/swagger.yml
270
http/swagger.yml
|
@ -4621,8 +4621,6 @@ paths:
|
|||
/packages:
|
||||
post:
|
||||
operationId: CreatePkg
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Create a new Influx package
|
||||
requestBody:
|
||||
description: Influx package to create.
|
||||
|
@ -4650,8 +4648,6 @@ paths:
|
|||
/packages/apply:
|
||||
post:
|
||||
operationId: ApplyPkg
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Apply or dry-run an Influx package
|
||||
requestBody:
|
||||
required: true
|
||||
|
@ -4694,8 +4690,6 @@ paths:
|
|||
/packages/stacks:
|
||||
get:
|
||||
operationId: ListStacks
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Grab a list of installed Influx packages
|
||||
parameters:
|
||||
- in: query
|
||||
|
@ -4734,8 +4728,6 @@ paths:
|
|||
$ref: "#/components/schemas/Error"
|
||||
post:
|
||||
operationId: CreateStack
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Create a new stack
|
||||
requestBody:
|
||||
description: Influx stack to create.
|
||||
|
@ -4771,8 +4763,6 @@ paths:
|
|||
/packages/stacks/{stack_id}:
|
||||
get:
|
||||
operationId: ReadStack
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Grab a stack by its ID
|
||||
parameters:
|
||||
- in: path
|
||||
|
@ -4796,8 +4786,6 @@ paths:
|
|||
$ref: "#/components/schemas/Error"
|
||||
patch:
|
||||
operationId: UpdateStack
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Update a an Influx Stack
|
||||
parameters:
|
||||
- in: path
|
||||
|
@ -4849,8 +4837,6 @@ paths:
|
|||
$ref: "#/components/schemas/Error"
|
||||
delete:
|
||||
operationId: DeleteStack
|
||||
tags:
|
||||
- InfluxPackages
|
||||
summary: Delete a stack and remove all its associated resources
|
||||
parameters:
|
||||
- in: path
|
||||
|
@ -4874,6 +4860,262 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/stacks:
|
||||
get:
|
||||
operationId: ListStacks
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Grab a list of installed InfluxDB Templates
|
||||
parameters:
|
||||
- in: query
|
||||
name: orgID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The organization id of the stacks
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
description: A collection of names to filter the list by.
|
||||
- in: query
|
||||
name: stackID
|
||||
schema:
|
||||
type: string
|
||||
description: A collection of stackIDs to filter the list by.
|
||||
responses:
|
||||
"200":
|
||||
description: Influx stacks found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
stacks:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Stack"
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
post:
|
||||
operationId: CreateStack
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Create a new stack
|
||||
requestBody:
|
||||
description: Stack to create.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
orgID:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
urls:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
"201":
|
||||
description: InfluxDB Stack created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Stack"
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/stacks/{stack_id}:
|
||||
get:
|
||||
operationId: ReadStack
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Grab a stack by its ID
|
||||
parameters:
|
||||
- in: path
|
||||
name: stack_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The stack id
|
||||
responses:
|
||||
"200":
|
||||
description: Read an influx stack by ID
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Stack"
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
patch:
|
||||
operationId: UpdateStack
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Update a an InfluxDB Stack
|
||||
parameters:
|
||||
- in: path
|
||||
name: stack_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The stack id
|
||||
requestBody:
|
||||
description: Influx stack to update.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
templateURLs:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
additionalResources:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
resourceID:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
templateMetaName:
|
||||
type: string
|
||||
required: ["kind","resourceID"]
|
||||
responses:
|
||||
"200":
|
||||
description: Influx stack updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Stack"
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
delete:
|
||||
operationId: DeleteStack
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Delete a stack and remove all its associated resources
|
||||
parameters:
|
||||
- in: path
|
||||
name: stack_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The stack id
|
||||
- in: query
|
||||
name: orgID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The organization id of the user
|
||||
responses:
|
||||
"204":
|
||||
description: Stack and all its associated resources are deleted
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/templates/apply:
|
||||
post:
|
||||
operationId: ApplyTemplate
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Apply or dry-run an InfluxDB Template
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PkgApply"
|
||||
application/x-jsonnet:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PkgApply"
|
||||
text/yml:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PkgApply"
|
||||
responses:
|
||||
"200":
|
||||
description: >
|
||||
Influx package dry-run successful, no new resources created.
|
||||
The provided diff and summary will not have IDs for resources
|
||||
that do not exist at the time of the dry run.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PkgSummary"
|
||||
"201":
|
||||
description: >
|
||||
Influx package applied successfully. Newly created resources created
|
||||
available in summary. The diff compares the state of the world before
|
||||
the package is applied with the changes the application will impose.
|
||||
This corresponds to `"dryRun": true`
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PkgSummary"
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/templates/export:
|
||||
post:
|
||||
operationId: ExportTemplate
|
||||
tags:
|
||||
- InfluxDB Templates
|
||||
summary: Export a new Influx Template
|
||||
requestBody:
|
||||
description: Export resources as an InfluxDB template.
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PkgCreate"
|
||||
responses:
|
||||
"200":
|
||||
description: InfluxDB template created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pkg"
|
||||
application/x-yaml:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pkg"
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/tasks:
|
||||
get:
|
||||
operationId: GetTasks
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
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"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
pctx "github.com/influxdata/influxdb/v2/context"
|
||||
ierrors "github.com/influxdata/influxdb/v2/kit/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"
|
||||
)
|
||||
|
@ -44,6 +36,9 @@ func NewHTTPServerPackages(log *zap.Logger, svc SVC) *HTTPServerPackages {
|
|||
setJSONContentType := middleware.SetHeader("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(
|
||||
middleware.SetHeader("Sunset", "Thurs, 30 July 2020 17:00:00 UTC"),
|
||||
)
|
||||
{
|
||||
r.With(exportAllowContentTypes).Post("/", svr.export)
|
||||
r.With(setJSONContentType).Post("/apply", svr.apply)
|
||||
|
@ -56,13 +51,7 @@ func NewHTTPServerPackages(log *zap.Logger, svc SVC) *HTTPServerPackages {
|
|||
r.Get("/", svr.readStack)
|
||||
r.Delete("/", svr.deleteStack)
|
||||
r.Patch("/", svr.updateStack)
|
||||
r.With(
|
||||
exportAllowContentTypes,
|
||||
// sunsetting, will not appear as part of the swagger doc and will be dropped
|
||||
// in the near term. The sunset header follows the following IETF RFC:
|
||||
// https://tools.ietf.org/html/rfc8594
|
||||
middleware.SetHeader("Sunset", "Thurs, 30 July 2020 17:00:00 UTC"),
|
||||
).Get("/export", svr.exportStack)
|
||||
r.With(exportAllowContentTypes).Get("/export", svr.exportStack)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -76,50 +65,6 @@ func (s *HTTPServerPackages) Prefix() string {
|
|||
return RoutePrefixPackages
|
||||
}
|
||||
|
||||
type (
|
||||
// RespStack is the response body for a stack.
|
||||
RespStack struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgID"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Resources []RespStackResource `json:"resources"`
|
||||
Sources []string `json:"sources"`
|
||||
URLs []string `json:"urls"`
|
||||
influxdb.CRUDLog
|
||||
}
|
||||
|
||||
// RespStackResource is the response for a stack resource. This type exists
|
||||
// to decouple the internal service implementation from the deprecates usage
|
||||
// of pkgs in the API. We could add a custom UnmarshalJSON method, but
|
||||
// I would rather keep it obvious and explicit with a separate field.
|
||||
RespStackResource struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
ID string `json:"resourceID"`
|
||||
Kind Kind `json:"kind"`
|
||||
MetaName string `json:"templateMetaName"`
|
||||
Associations []RespStackResourceAssoc `json:"associations"`
|
||||
|
||||
// PkgName is deprecated moving forward, will support until it is
|
||||
// ripped out.
|
||||
PkgName *string `json:"pkgName,omitempty"`
|
||||
}
|
||||
|
||||
// RespStackResourceAssoc is the response for a stack resource's associations.
|
||||
RespStackResourceAssoc struct {
|
||||
Kind Kind `json:"kind"`
|
||||
MetaName string `json:"metaName"`
|
||||
|
||||
//PkgName is to be deprecated moving forward
|
||||
PkgName *string `json:"pkgName,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// RespListStacks is the HTTP response for a stack list call.
|
||||
type RespListStacks struct {
|
||||
Stacks []RespStack `json:"stacks"`
|
||||
}
|
||||
|
||||
func (s *HTTPServerPackages) listStacks(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
|
||||
|
@ -179,40 +124,6 @@ func (s *HTTPServerPackages) listStacks(w http.ResponseWriter, r *http.Request)
|
|||
})
|
||||
}
|
||||
|
||||
// ReqCreateStack is a request body for a create stack call.
|
||||
type ReqCreateStack struct {
|
||||
OrgID string `json:"orgID"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
// OK validates the request body is valid.
|
||||
func (r *ReqCreateStack) OK() error {
|
||||
// TODO: provide multiple errors back for failing validation
|
||||
if _, err := influxdb.IDFromString(r.OrgID); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("provided org id[%q] is invalid", r.OrgID),
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range r.URLs {
|
||||
if _, err := url.Parse(u); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("provided url[%q] is invalid", u),
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReqCreateStack) orgID() influxdb.ID {
|
||||
orgID, _ := influxdb.IDFromString(r.OrgID)
|
||||
return *orgID
|
||||
}
|
||||
|
||||
func (s *HTTPServerPackages) createStack(w http.ResponseWriter, r *http.Request) {
|
||||
var reqBody ReqCreateStack
|
||||
if err := s.api.DecodeJSON(r.Body, &reqBody); err != nil {
|
||||
|
@ -321,26 +232,6 @@ func (s *HTTPServerPackages) readStack(w http.ResponseWriter, r *http.Request) {
|
|||
s.api.Respond(w, r, http.StatusOK, convertStackToRespStack(stack))
|
||||
}
|
||||
|
||||
type (
|
||||
// ReqUpdateStack is the request body for updating a stack.
|
||||
ReqUpdateStack struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
TemplateURLs []string `json:"templateURLs"`
|
||||
AdditionalResources []ReqUpdateStackResource `json:"additionalResources"`
|
||||
|
||||
// Deprecating the urls field and replacing with templateURLs field.
|
||||
// This is remaining here for backwards compatibility.
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
ReqUpdateStackResource struct {
|
||||
ID string `json:"resourceID"`
|
||||
Kind Kind `json:"kind"`
|
||||
MetaName string `json:"templateMetaName"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *HTTPServerPackages) updateStack(w http.ResponseWriter, r *http.Request) {
|
||||
var req ReqUpdateStack
|
||||
if err := s.api.DecodeJSON(r.Body, &req); err != nil {
|
||||
|
@ -383,82 +274,6 @@ func (s *HTTPServerPackages) updateStack(w http.ResponseWriter, r *http.Request)
|
|||
s.api.Respond(w, r, http.StatusOK, convertStackToRespStack(stack))
|
||||
}
|
||||
|
||||
func stackIDFromReq(r *http.Request) (influxdb.ID, error) {
|
||||
stackID, err := influxdb.IDFromString(chi.URLParam(r, "stack_id"))
|
||||
if err != nil {
|
||||
return 0, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "the stack id provided in the path was invalid",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return *stackID, nil
|
||||
}
|
||||
|
||||
func getRequiredOrgIDFromQuery(q url.Values) (influxdb.ID, error) {
|
||||
orgIDRaw := q.Get("orgID")
|
||||
if orgIDRaw == "" {
|
||||
return 0, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "the orgID query param is required",
|
||||
}
|
||||
}
|
||||
|
||||
orgID, err := influxdb.IDFromString(orgIDRaw)
|
||||
if err != nil {
|
||||
return 0, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "the orgID query param was invalid",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return *orgID, nil
|
||||
}
|
||||
|
||||
// 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 &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: "at least 1 resource, 1 org id, or stack id must be provided",
|
||||
}
|
||||
}
|
||||
|
||||
for _, org := range r.OrgIDs {
|
||||
if _, err := influxdb.IDFromString(org.OrgID); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("provided org id is invalid: %q", org.OrgID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.StackID != "" {
|
||||
_, err := influxdb.IDFromString(r.StackID)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RespExport is a response body for the create pkg endpoint.
|
||||
type RespExport []Object
|
||||
|
||||
func (s *HTTPServerPackages) export(w http.ResponseWriter, r *http.Request) {
|
||||
var reqBody ReqExport
|
||||
if err := s.api.DecodeJSON(r.Body, &reqBody); err != nil {
|
||||
|
@ -518,187 +333,6 @@ func (s *HTTPServerPackages) export(w http.ResponseWriter, r *http.Request) {
|
|||
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"`
|
||||
Pkg 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 pkg 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"`
|
||||
|
||||
// TODO(jsteenb2): pkg references will all be replaced by template references
|
||||
// these 2 exist alongside the templates for backwards compatibility
|
||||
// until beta13 rolls out the door. This code should get axed when the next
|
||||
// OSS release goes out.
|
||||
RawPkgs []json.RawMessage `json:"packages" yaml:"packages"`
|
||||
RawPkg json.RawMessage `json:"package" yaml:"package"`
|
||||
|
||||
RawTemplates []ReqRawTemplate `json:"templates" yaml:"templates"`
|
||||
RawTemplate ReqRawTemplate `json:"template" yaml:"template"`
|
||||
|
||||
EnvRefs map[string]string `json:"envRefs"`
|
||||
Secrets map[string]string `json:"secrets"`
|
||||
|
||||
RawActions []ReqRawAction `json:"actions"`
|
||||
}
|
||||
|
||||
// Pkgs returns all pkgs associated with the request.
|
||||
func (r ReqApply) Pkgs(encoding Encoding) (*Pkg, error) {
|
||||
var rawPkgs []*Pkg
|
||||
for _, rem := range r.Remotes {
|
||||
if rem.URL == "" {
|
||||
continue
|
||||
}
|
||||
pkg, err := Parse(rem.Encoding(), FromHTTPRequest(rem.URL), ValidSkipParseError())
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: fmt.Sprintf("pkg from url[%s] had an issue: %s", rem.URL, err.Error()),
|
||||
}
|
||||
}
|
||||
rawPkgs = append(rawPkgs, pkg)
|
||||
}
|
||||
|
||||
for i, rawPkg := range append(r.RawPkgs, r.RawPkg) {
|
||||
if rawPkg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg, err := Parse(encoding, FromReader(bytes.NewReader(rawPkg)), ValidSkipParseError())
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: fmt.Sprintf("pkg[%d] had an issue: %s", i, err.Error()),
|
||||
}
|
||||
}
|
||||
rawPkgs = append(rawPkgs, pkg)
|
||||
}
|
||||
|
||||
for i, rawTmpl := range append(r.RawTemplates, r.RawTemplate) {
|
||||
if rawTmpl.Pkg == nil {
|
||||
continue
|
||||
}
|
||||
enc := encoding
|
||||
if sourceEncoding := rawTmpl.Encoding(); sourceEncoding != EncodingSource {
|
||||
enc = sourceEncoding
|
||||
}
|
||||
pkg, err := Parse(enc, FromReader(bytes.NewReader(rawTmpl.Pkg), rawTmpl.Sources...), ValidSkipParseError())
|
||||
if err != nil {
|
||||
sources := formatSources(rawTmpl.Sources)
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: fmt.Sprintf("pkg[%d] from source(s) %q had an issue: %s", i, sources, err.Error()),
|
||||
}
|
||||
}
|
||||
rawPkgs = append(rawPkgs, pkg)
|
||||
}
|
||||
|
||||
return Combine(rawPkgs, 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(influxdb.EInvalid, unmarshalErrFn(err, i, a))
|
||||
}
|
||||
if err := asr.Kind.OK(); err != nil {
|
||||
return actions{}, influxErr(influxdb.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(influxdb.EInvalid, unmarshalErrFn(err, i, a))
|
||||
}
|
||||
if err := ask.Kind.OK(); err != nil {
|
||||
return actions{}, influxErr(influxdb.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(influxdb.EInvalid, msg)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RespApply is the response body for the apply pkg 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"`
|
||||
}
|
||||
|
||||
func (s *HTTPServerPackages) apply(w http.ResponseWriter, r *http.Request) {
|
||||
var reqBody ReqApply
|
||||
encoding, err := decodeWithEncoding(r, &reqBody)
|
||||
|
@ -804,62 +438,6 @@ func (s *HTTPServerPackages) apply(w http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
type encoder interface {
|
||||
Encode(interface{}) error
|
||||
}
|
||||
|
||||
func formatSources(sources []string) string {
|
||||
return strings.Join(sources, "; ")
|
||||
}
|
||||
|
||||
func decodeWithEncoding(r *http.Request, v interface{}) (Encoding, error) {
|
||||
encoding := pkgEncoding(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 pkgEncoding(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
|
||||
}
|
||||
}
|
||||
|
||||
func newJSONEnc(w io.Writer) encoder {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", "\t")
|
||||
return enc
|
||||
}
|
||||
|
||||
func (s *HTTPServerPackages) encResp(w http.ResponseWriter, r *http.Request, enc encoder, code int, res interface{}) {
|
||||
w.WriteHeader(code)
|
||||
if err := enc.Encode(res); err != nil {
|
||||
|
@ -871,22 +449,6 @@ func (s *HTTPServerPackages) encResp(w http.ResponseWriter, r *http.Request, enc
|
|||
}
|
||||
}
|
||||
|
||||
func convertParseErr(err error) []ValidationErr {
|
||||
pErr, ok := err.(ParseError)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return pErr.ValidationErrs()
|
||||
}
|
||||
|
||||
func newDecodeErr(encoding string, err error) *influxdb.Error {
|
||||
return &influxdb.Error{
|
||||
Msg: fmt.Sprintf("unable to unmarshal %s", encoding),
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func convertStackToRespStack(st Stack) RespStack {
|
||||
resources := make([]RespStackResource, 0, len(st.Resources))
|
||||
for _, r := range st.Resources {
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
package pkger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
pctx "github.com/influxdata/influxdb/v2/context"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const RoutePrefixStacks = "/api/v2/stacks"
|
||||
|
||||
// HTTPServerStacks is a server that manages the stacks HTTP transport.
|
||||
type HTTPServerStacks struct {
|
||||
chi.Router
|
||||
api *kithttp.API
|
||||
logger *zap.Logger
|
||||
svc SVC
|
||||
}
|
||||
|
||||
// NewHTTPServerStacks constructs a new http server.
|
||||
func NewHTTPServerStacks(log *zap.Logger, svc SVC) *HTTPServerStacks {
|
||||
svr := &HTTPServerStacks{
|
||||
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
||||
logger: log,
|
||||
svc: svc,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
{
|
||||
r.Post("/", svr.createStack)
|
||||
r.Get("/", svr.listStacks)
|
||||
|
||||
r.Route("/{stack_id}", func(r chi.Router) {
|
||||
r.Get("/", svr.readStack)
|
||||
r.Delete("/", svr.deleteStack)
|
||||
r.Patch("/", svr.updateStack)
|
||||
})
|
||||
}
|
||||
|
||||
svr.Router = r
|
||||
return svr
|
||||
}
|
||||
|
||||
// Prefix provides the prefix to this route tree.
|
||||
func (s *HTTPServerStacks) Prefix() string {
|
||||
return RoutePrefixStacks
|
||||
}
|
||||
|
||||
type (
|
||||
// RespStack is the response body for a stack.
|
||||
RespStack struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgID"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Resources []RespStackResource `json:"resources"`
|
||||
Sources []string `json:"sources"`
|
||||
URLs []string `json:"urls"`
|
||||
influxdb.CRUDLog
|
||||
}
|
||||
|
||||
// RespStackResource is the response for a stack resource. This type exists
|
||||
// to decouple the internal service implementation from the deprecates usage
|
||||
// of pkgs in the API. We could add a custom UnmarshalJSON method, but
|
||||
// I would rather keep it obvious and explicit with a separate field.
|
||||
RespStackResource struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
ID string `json:"resourceID"`
|
||||
Kind Kind `json:"kind"`
|
||||
MetaName string `json:"templateMetaName"`
|
||||
Associations []RespStackResourceAssoc `json:"associations"`
|
||||
|
||||
// PkgName is deprecated moving forward, will support until it is
|
||||
// ripped out.
|
||||
PkgName *string `json:"pkgName,omitempty"`
|
||||
}
|
||||
|
||||
// RespStackResourceAssoc is the response for a stack resource's associations.
|
||||
RespStackResourceAssoc struct {
|
||||
Kind Kind `json:"kind"`
|
||||
MetaName string `json:"metaName"`
|
||||
|
||||
//PkgName is to be deprecated moving forward
|
||||
PkgName *string `json:"pkgName,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// RespListStacks is the HTTP response for a stack list call.
|
||||
type RespListStacks struct {
|
||||
Stacks []RespStack `json:"stacks"`
|
||||
}
|
||||
|
||||
func (s *HTTPServerStacks) listStacks(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
|
||||
rawOrgID := q.Get("orgID")
|
||||
orgID, err := influxdb.IDFromString(rawOrgID)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("organization id[%q] is invalid", rawOrgID),
|
||||
Err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "failed to parse form from encoded url",
|
||||
Err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
filter := ListFilter{
|
||||
Names: r.Form["name"],
|
||||
}
|
||||
|
||||
for _, idRaw := range r.Form["stackID"] {
|
||||
id, err := influxdb.IDFromString(idRaw)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("stack ID[%q] provided is invalid", idRaw),
|
||||
Err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
filter.StackIDs = append(filter.StackIDs, *id)
|
||||
}
|
||||
|
||||
stacks, err := s.svc.ListStacks(r.Context(), *orgID, filter)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
if stacks == nil {
|
||||
stacks = []Stack{}
|
||||
}
|
||||
|
||||
out := make([]RespStack, 0, len(stacks))
|
||||
for _, st := range stacks {
|
||||
out = append(out, convertStackToRespStack(st))
|
||||
}
|
||||
|
||||
s.api.Respond(w, r, http.StatusOK, RespListStacks{
|
||||
Stacks: out,
|
||||
})
|
||||
}
|
||||
|
||||
// ReqCreateStack is a request body for a create stack call.
|
||||
type ReqCreateStack struct {
|
||||
OrgID string `json:"orgID"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
// OK validates the request body is valid.
|
||||
func (r *ReqCreateStack) OK() error {
|
||||
// TODO: provide multiple errors back for failing validation
|
||||
if _, err := influxdb.IDFromString(r.OrgID); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("provided org id[%q] is invalid", r.OrgID),
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range r.URLs {
|
||||
if _, err := url.Parse(u); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("provided url[%q] is invalid", u),
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReqCreateStack) orgID() influxdb.ID {
|
||||
orgID, _ := influxdb.IDFromString(r.OrgID)
|
||||
return *orgID
|
||||
}
|
||||
|
||||
func (s *HTTPServerStacks) createStack(w http.ResponseWriter, r *http.Request) {
|
||||
var reqBody ReqCreateStack
|
||||
if err := s.api.DecodeJSON(r.Body, &reqBody); err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
auth, err := pctx.GetAuthorizer(r.Context())
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := s.svc.InitStack(r.Context(), auth.GetUserID(), Stack{
|
||||
OrgID: reqBody.orgID(),
|
||||
Name: reqBody.Name,
|
||||
Description: reqBody.Description,
|
||||
TemplateURLs: reqBody.URLs,
|
||||
})
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.api.Respond(w, r, http.StatusCreated, convertStackToRespStack(stack))
|
||||
}
|
||||
|
||||
func (s *HTTPServerStacks) deleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := getRequiredOrgIDFromQuery(r.URL.Query())
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := stackIDFromReq(r)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
auth, err := pctx.GetAuthorizer(r.Context())
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
userID := auth.GetUserID()
|
||||
|
||||
err = s.svc.DeleteStack(r.Context(), struct{ OrgID, UserID, StackID influxdb.ID }{
|
||||
OrgID: orgID,
|
||||
UserID: userID,
|
||||
StackID: stackID,
|
||||
})
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.api.Respond(w, r, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (s *HTTPServerStacks) readStack(w http.ResponseWriter, r *http.Request) {
|
||||
stackID, err := stackIDFromReq(r)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := s.svc.ReadStack(r.Context(), stackID)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.api.Respond(w, r, http.StatusOK, convertStackToRespStack(stack))
|
||||
}
|
||||
|
||||
type (
|
||||
// ReqUpdateStack is the request body for updating a stack.
|
||||
ReqUpdateStack struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
TemplateURLs []string `json:"templateURLs"`
|
||||
AdditionalResources []ReqUpdateStackResource `json:"additionalResources"`
|
||||
|
||||
// Deprecating the urls field and replacing with templateURLs field.
|
||||
// This is remaining here for backwards compatibility.
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
ReqUpdateStackResource struct {
|
||||
ID string `json:"resourceID"`
|
||||
Kind Kind `json:"kind"`
|
||||
MetaName string `json:"templateMetaName"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *HTTPServerStacks) updateStack(w http.ResponseWriter, r *http.Request) {
|
||||
var req ReqUpdateStack
|
||||
if err := s.api.DecodeJSON(r.Body, &req); err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := stackIDFromReq(r)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
update := StackUpdate{
|
||||
ID: stackID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
TemplateURLs: append(req.TemplateURLs, req.URLs...),
|
||||
}
|
||||
for _, res := range req.AdditionalResources {
|
||||
id, err := influxdb.IDFromString(res.ID)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, influxErr(influxdb.EInvalid, err, fmt.Sprintf("stack resource id %q", res.ID)))
|
||||
return
|
||||
}
|
||||
update.AdditionalResources = append(update.AdditionalResources, StackAdditionalResource{
|
||||
APIVersion: APIVersion,
|
||||
ID: *id,
|
||||
Kind: res.Kind,
|
||||
MetaName: res.MetaName,
|
||||
})
|
||||
}
|
||||
|
||||
stack, err := s.svc.UpdateStack(r.Context(), update)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.api.Respond(w, r, http.StatusOK, convertStackToRespStack(stack))
|
||||
}
|
||||
|
||||
func stackIDFromReq(r *http.Request) (influxdb.ID, error) {
|
||||
stackID, err := influxdb.IDFromString(chi.URLParam(r, "stack_id"))
|
||||
if err != nil {
|
||||
return 0, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "the stack id provided in the path was invalid",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return *stackID, nil
|
||||
}
|
||||
|
||||
func getRequiredOrgIDFromQuery(q url.Values) (influxdb.ID, error) {
|
||||
orgIDRaw := q.Get("orgID")
|
||||
if orgIDRaw == "" {
|
||||
return 0, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "the orgID query param is required",
|
||||
}
|
||||
}
|
||||
|
||||
orgID, err := influxdb.IDFromString(orgIDRaw)
|
||||
if err != nil {
|
||||
return 0, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "the orgID query param was invalid",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return *orgID, nil
|
||||
}
|
|
@ -0,0 +1,596 @@
|
|||
package pkger_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/pkg/testttp"
|
||||
"github.com/influxdata/influxdb/v2/pkger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestPkgerHTTPServerStacks(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := ioutil.ReadFile(strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
})
|
||||
filesvr := httptest.NewServer(mux)
|
||||
defer filesvr.Close()
|
||||
|
||||
strPtr := func(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
t.Run("create a stack", func(t *testing.T) {
|
||||
t.Run("should successfully return with valid req body", func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
|
||||
stack.ID = 3
|
||||
stack.CreatedAt = time.Now()
|
||||
stack.UpdatedAt = time.Now()
|
||||
return stack, nil
|
||||
},
|
||||
}
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
reqBody := pkger.ReqCreateStack{
|
||||
OrgID: influxdb.ID(3).String(),
|
||||
Name: "threeve",
|
||||
Description: "desc",
|
||||
URLs: []string{"http://example.com"},
|
||||
}
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/stacks", reqBody).
|
||||
Headers("Content-Type", "application/json").
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusCreated).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespStack
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.NotZero(t, resp.ID)
|
||||
assert.Equal(t, reqBody.OrgID, resp.OrgID)
|
||||
assert.Equal(t, reqBody.Name, resp.Name)
|
||||
assert.Equal(t, reqBody.Description, resp.Description)
|
||||
assert.Equal(t, reqBody.URLs, resp.URLs)
|
||||
assert.NotZero(t, resp.CRUDLog)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("error cases", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reqBody pkger.ReqCreateStack
|
||||
expectedStatus int
|
||||
svc pkger.SVC
|
||||
}{
|
||||
{
|
||||
name: "bad org id",
|
||||
reqBody: pkger.ReqCreateStack{
|
||||
OrgID: "invalid id",
|
||||
},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "bad url",
|
||||
reqBody: pkger.ReqCreateStack{
|
||||
OrgID: influxdb.ID(3).String(),
|
||||
URLs: []string{"invalid @% url"},
|
||||
},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "translates svc conflict error",
|
||||
reqBody: pkger.ReqCreateStack{OrgID: influxdb.ID(3).String()},
|
||||
svc: &fakeSVC{
|
||||
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
|
||||
return pkger.Stack{}, &influxdb.Error{Code: influxdb.EConflict}
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "translates svc internal error",
|
||||
reqBody: pkger.ReqCreateStack{OrgID: influxdb.ID(3).String()},
|
||||
svc: &fakeSVC{
|
||||
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
|
||||
return pkger.Stack{}, &influxdb.Error{Code: influxdb.EInternal}
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := tt.svc
|
||||
if svc == nil {
|
||||
svc = &fakeSVC{
|
||||
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
|
||||
return stack, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/stacks", tt.reqBody).
|
||||
Headers("Content-Type", "application/json").
|
||||
Do(svr).
|
||||
ExpectStatus(tt.expectedStatus)
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("list a stack", func(t *testing.T) {
|
||||
t.Run("should successfully return with valid req body", func(t *testing.T) {
|
||||
const expectedOrgID influxdb.ID = 3
|
||||
|
||||
svc := &fakeSVC{
|
||||
listStacksFn: func(ctx context.Context, orgID influxdb.ID, filter pkger.ListFilter) ([]pkger.Stack, error) {
|
||||
if orgID != expectedOrgID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(filter.Names) > 0 && len(filter.StackIDs) == 0 {
|
||||
var stacks []pkger.Stack
|
||||
for i, name := range filter.Names {
|
||||
stacks = append(stacks, pkger.Stack{
|
||||
ID: influxdb.ID(i + 1),
|
||||
OrgID: expectedOrgID,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
if len(filter.StackIDs) > 0 && len(filter.Names) == 0 {
|
||||
var stacks []pkger.Stack
|
||||
for _, stackID := range filter.StackIDs {
|
||||
stacks = append(stacks, pkger.Stack{
|
||||
ID: stackID,
|
||||
OrgID: expectedOrgID,
|
||||
})
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
return []pkger.Stack{{
|
||||
ID: 1,
|
||||
OrgID: expectedOrgID,
|
||||
Name: "stack_1",
|
||||
}}, nil
|
||||
},
|
||||
}
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
queryArgs string
|
||||
expectedStacks []pkger.RespStack
|
||||
}{
|
||||
{
|
||||
name: "with org ID that has stacks",
|
||||
queryArgs: "orgID=" + expectedOrgID.String(),
|
||||
expectedStacks: []pkger.RespStack{{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "stack_1",
|
||||
Resources: []pkger.RespStackResource{},
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "with orgID with no stacks",
|
||||
queryArgs: "orgID=" + influxdb.ID(9000).String(),
|
||||
expectedStacks: []pkger.RespStack{},
|
||||
},
|
||||
{
|
||||
name: "with names",
|
||||
queryArgs: "name=name_stack&name=threeve&orgID=" + influxdb.ID(expectedOrgID).String(),
|
||||
expectedStacks: []pkger.RespStack{
|
||||
{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "name_stack",
|
||||
Resources: []pkger.RespStackResource{},
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
},
|
||||
{
|
||||
ID: influxdb.ID(2).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "threeve",
|
||||
Resources: []pkger.RespStackResource{},
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with ids",
|
||||
queryArgs: fmt.Sprintf("stackID=%s&stackID=%s&orgID=%s", influxdb.ID(1), influxdb.ID(2), influxdb.ID(expectedOrgID)),
|
||||
expectedStacks: []pkger.RespStack{
|
||||
{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Resources: []pkger.RespStackResource{},
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
},
|
||||
{
|
||||
ID: influxdb.ID(2).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Resources: []pkger.RespStackResource{},
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
testttp.
|
||||
Get(t, "/api/v2/stacks?"+tt.queryArgs).
|
||||
Headers("Content-Type", "application/x-www-form-urlencoded").
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespListStacks
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.Equal(t, tt.expectedStacks, resp.Stacks)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("read a stack", func(t *testing.T) {
|
||||
t.Run("should successfully return with valid req body", func(t *testing.T) {
|
||||
const expectedOrgID influxdb.ID = 3
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stub pkger.Stack
|
||||
expectedStack pkger.RespStack
|
||||
}{
|
||||
{
|
||||
name: "for stack that has all fields available",
|
||||
stub: pkger.Stack{
|
||||
ID: 1,
|
||||
OrgID: expectedOrgID,
|
||||
Name: "name",
|
||||
Description: "desc",
|
||||
Sources: []string{"threeve"},
|
||||
TemplateURLs: []string{"http://example.com"},
|
||||
Resources: []pkger.StackResource{
|
||||
{
|
||||
APIVersion: pkger.APIVersion,
|
||||
ID: 3,
|
||||
Kind: pkger.KindBucket,
|
||||
MetaName: "rucketeer",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "name",
|
||||
Description: "desc",
|
||||
Sources: []string{"threeve"},
|
||||
URLs: []string{"http://example.com"},
|
||||
Resources: []pkger.RespStackResource{
|
||||
{
|
||||
APIVersion: pkger.APIVersion,
|
||||
ID: influxdb.ID(3).String(),
|
||||
Kind: pkger.KindBucket,
|
||||
MetaName: "rucketeer",
|
||||
Associations: []pkger.RespStackResourceAssoc{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "for stack that has missing resources urls and sources",
|
||||
stub: pkger.Stack{
|
||||
ID: 1,
|
||||
OrgID: expectedOrgID,
|
||||
Name: "name",
|
||||
Description: "desc",
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "name",
|
||||
Description: "desc",
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
Resources: []pkger.RespStackResource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "for stack that has no set fields",
|
||||
stub: pkger.Stack{
|
||||
ID: 1,
|
||||
OrgID: expectedOrgID,
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
Resources: []pkger.RespStackResource{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
readStackFn: func(ctx context.Context, id influxdb.ID) (pkger.Stack, error) {
|
||||
return tt.stub, nil
|
||||
},
|
||||
}
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
Get(t, "/api/v2/stacks/"+tt.stub.ID.String()).
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespStack
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.Equal(t, tt.expectedStack, resp)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error cases", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stackIDPath string
|
||||
expectedStatus int
|
||||
svc pkger.SVC
|
||||
}{
|
||||
{
|
||||
name: "bad stack id path",
|
||||
stackIDPath: "badID",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "stack not found",
|
||||
stackIDPath: influxdb.ID(1).String(),
|
||||
svc: &fakeSVC{
|
||||
readStackFn: func(ctx context.Context, id influxdb.ID) (pkger.Stack, error) {
|
||||
return pkger.Stack{}, &influxdb.Error{Code: influxdb.ENotFound}
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := tt.svc
|
||||
if svc == nil {
|
||||
svc = &fakeSVC{
|
||||
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
|
||||
return stack, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
Get(t, "/api/v2/stacks/"+tt.stackIDPath).
|
||||
Headers("Content-Type", "application/json").
|
||||
Do(svr).
|
||||
ExpectStatus(tt.expectedStatus)
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("update a stack", func(t *testing.T) {
|
||||
t.Run("should successfully update with valid req body", func(t *testing.T) {
|
||||
const expectedOrgID influxdb.ID = 3
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkger.ReqUpdateStack
|
||||
expectedStack pkger.RespStack
|
||||
}{
|
||||
{
|
||||
name: "update name field",
|
||||
input: pkger.ReqUpdateStack{
|
||||
Name: strPtr("name"),
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "name",
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
Resources: []pkger.RespStackResource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update desc field",
|
||||
input: pkger.ReqUpdateStack{
|
||||
Description: strPtr("desc"),
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Description: "desc",
|
||||
Sources: []string{},
|
||||
URLs: []string{},
|
||||
Resources: []pkger.RespStackResource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update urls field",
|
||||
input: pkger.ReqUpdateStack{
|
||||
TemplateURLs: []string{"http://example.com"},
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Sources: []string{},
|
||||
URLs: []string{"http://example.com"},
|
||||
Resources: []pkger.RespStackResource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update all fields",
|
||||
input: pkger.ReqUpdateStack{
|
||||
Name: strPtr("name"),
|
||||
Description: strPtr("desc"),
|
||||
TemplateURLs: []string{"http://example.com"},
|
||||
},
|
||||
expectedStack: pkger.RespStack{
|
||||
ID: influxdb.ID(1).String(),
|
||||
OrgID: expectedOrgID.String(),
|
||||
Name: "name",
|
||||
Description: "desc",
|
||||
Sources: []string{},
|
||||
URLs: []string{"http://example.com"},
|
||||
Resources: []pkger.RespStackResource{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
id, err := influxdb.IDFromString(tt.expectedStack.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
svc := &fakeSVC{
|
||||
updateStackFn: func(ctx context.Context, upd pkger.StackUpdate) (pkger.Stack, error) {
|
||||
if upd.ID != *id {
|
||||
return pkger.Stack{}, errors.New("unexpected stack ID: " + upd.ID.String())
|
||||
}
|
||||
st := pkger.Stack{
|
||||
ID: *id,
|
||||
OrgID: expectedOrgID,
|
||||
}
|
||||
if upd.Name != nil {
|
||||
st.Name = *upd.Name
|
||||
}
|
||||
if upd.Description != nil {
|
||||
st.Description = *upd.Description
|
||||
}
|
||||
if upd.TemplateURLs != nil {
|
||||
st.TemplateURLs = upd.TemplateURLs
|
||||
}
|
||||
return st, nil
|
||||
},
|
||||
}
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PatchJSON(t, "/api/v2/stacks/"+tt.expectedStack.ID, tt.input).
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespStack
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.Equal(t, tt.expectedStack, resp)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error cases", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stackIDPath string
|
||||
expectedStatus int
|
||||
svc pkger.SVC
|
||||
}{
|
||||
{
|
||||
name: "bad stack id path",
|
||||
stackIDPath: "badID",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "stack not found",
|
||||
stackIDPath: influxdb.ID(1).String(),
|
||||
svc: &fakeSVC{
|
||||
readStackFn: func(ctx context.Context, id influxdb.ID) (pkger.Stack, error) {
|
||||
return pkger.Stack{}, &influxdb.Error{Code: influxdb.ENotFound}
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := tt.svc
|
||||
if svc == nil {
|
||||
svc = &fakeSVC{
|
||||
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
|
||||
return stack, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
Get(t, "/api/v2/stacks/"+tt.stackIDPath).
|
||||
Headers("Content-Type", "application/json").
|
||||
Do(svr).
|
||||
ExpectStatus(tt.expectedStatus)
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,529 @@
|
|||
package pkger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
pctx "github.com/influxdata/influxdb/v2/context"
|
||||
ierrors "github.com/influxdata/influxdb/v2/kit/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
|
||||
}
|
||||
|
||||
// NewHTTPServerTemplates constructs a new http server.
|
||||
func NewHTTPServerTemplates(log *zap.Logger, svc SVC) *HTTPServerTemplates {
|
||||
svr := &HTTPServerTemplates{
|
||||
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
||||
logger: log,
|
||||
svc: svc,
|
||||
}
|
||||
|
||||
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 &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: "at least 1 resource, 1 org id, or stack id must be provided",
|
||||
}
|
||||
}
|
||||
|
||||
for _, org := range r.OrgIDs {
|
||||
if _, err := influxdb.IDFromString(org.OrgID); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("provided org id is invalid: %q", org.OrgID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.StackID != "" {
|
||||
_, err := influxdb.IDFromString(r.StackID)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RespExport is a response body for the create pkg 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 := influxdb.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 := influxdb.IDFromString(reqBody.StackID)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("invalid stack ID provided: %q", reqBody.StackID),
|
||||
})
|
||||
return
|
||||
}
|
||||
opts = append(opts, ExportWithStackID(*stackID))
|
||||
}
|
||||
|
||||
newPkg, err := s.svc.Export(r.Context(), opts...)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := RespExport(newPkg.Objects)
|
||||
if resp == nil {
|
||||
resp = []Object{}
|
||||
}
|
||||
|
||||
var enc encoder
|
||||
switch pkgEncoding(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"`
|
||||
Pkg 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 pkg 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"`
|
||||
|
||||
// TODO(jsteenb2): pkg references will all be replaced by template references
|
||||
// these 2 exist alongside the templates for backwards compatibility
|
||||
// until beta13 rolls out the door. This code should get axed when the next
|
||||
// OSS release goes out.
|
||||
RawPkgs []json.RawMessage `json:"packages" yaml:"packages"`
|
||||
RawPkg json.RawMessage `json:"package" yaml:"package"`
|
||||
|
||||
RawTemplates []ReqRawTemplate `json:"templates" yaml:"templates"`
|
||||
RawTemplate ReqRawTemplate `json:"template" yaml:"template"`
|
||||
|
||||
EnvRefs map[string]string `json:"envRefs"`
|
||||
Secrets map[string]string `json:"secrets"`
|
||||
|
||||
RawActions []ReqRawAction `json:"actions"`
|
||||
}
|
||||
|
||||
// Pkgs returns all pkgs associated with the request.
|
||||
func (r ReqApply) Pkgs(encoding Encoding) (*Pkg, error) {
|
||||
var rawPkgs []*Pkg
|
||||
for _, rem := range r.Remotes {
|
||||
if rem.URL == "" {
|
||||
continue
|
||||
}
|
||||
pkg, err := Parse(rem.Encoding(), FromHTTPRequest(rem.URL), ValidSkipParseError())
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: fmt.Sprintf("pkg from url[%s] had an issue: %s", rem.URL, err.Error()),
|
||||
}
|
||||
}
|
||||
rawPkgs = append(rawPkgs, pkg)
|
||||
}
|
||||
|
||||
for i, rawPkg := range append(r.RawPkgs, r.RawPkg) {
|
||||
if rawPkg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg, err := Parse(encoding, FromReader(bytes.NewReader(rawPkg)), ValidSkipParseError())
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: fmt.Sprintf("pkg[%d] had an issue: %s", i, err.Error()),
|
||||
}
|
||||
}
|
||||
rawPkgs = append(rawPkgs, pkg)
|
||||
}
|
||||
|
||||
for i, rawTmpl := range append(r.RawTemplates, r.RawTemplate) {
|
||||
if rawTmpl.Pkg == nil {
|
||||
continue
|
||||
}
|
||||
enc := encoding
|
||||
if sourceEncoding := rawTmpl.Encoding(); sourceEncoding != EncodingSource {
|
||||
enc = sourceEncoding
|
||||
}
|
||||
pkg, err := Parse(enc, FromReader(bytes.NewReader(rawTmpl.Pkg), rawTmpl.Sources...), ValidSkipParseError())
|
||||
if err != nil {
|
||||
sources := formatSources(rawTmpl.Sources)
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Msg: fmt.Sprintf("pkg[%d] from source(s) %q had an issue: %s", i, sources, err.Error()),
|
||||
}
|
||||
}
|
||||
rawPkgs = append(rawPkgs, pkg)
|
||||
}
|
||||
|
||||
return Combine(rawPkgs, 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(influxdb.EInvalid, unmarshalErrFn(err, i, a))
|
||||
}
|
||||
if err := asr.Kind.OK(); err != nil {
|
||||
return actions{}, influxErr(influxdb.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(influxdb.EInvalid, unmarshalErrFn(err, i, a))
|
||||
}
|
||||
if err := ask.Kind.OK(); err != nil {
|
||||
return actions{}, influxErr(influxdb.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(influxdb.EInvalid, msg)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RespApply is the response body for the apply pkg 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"`
|
||||
}
|
||||
|
||||
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 := influxdb.IDFromString(reqBody.OrgID)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("invalid organization ID provided: %q", reqBody.OrgID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var stackID influxdb.ID
|
||||
if reqBody.StackID != nil {
|
||||
if err := stackID.DecodeFromString(*reqBody.StackID); err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: fmt.Sprintf("invalid stack ID provided: %q", *reqBody.StackID),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
parsedPkg, err := reqBody.Pkgs(encoding)
|
||||
if err != nil {
|
||||
s.api.Err(w, r, &influxdb.Error{
|
||||
Code: influxdb.EUnprocessableEntity,
|
||||
Err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
actions, err := reqBody.validActions()
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
applyOpts := []ApplyOptFn{
|
||||
ApplyWithEnvRefs(reqBody.EnvRefs),
|
||||
ApplyWithPkg(parsedPkg),
|
||||
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, RespApply{
|
||||
Sources: append([]string{}, impact.Sources...), // guarantee non nil slice
|
||||
StackID: impact.StackID.String(),
|
||||
Diff: impact.Diff,
|
||||
Summary: impact.Summary,
|
||||
Errors: convertParseErr(err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.api.Respond(w, r, http.StatusOK, RespApply{
|
||||
Sources: append([]string{}, impact.Sources...), // guarantee non nil slice
|
||||
StackID: impact.StackID.String(),
|
||||
Diff: impact.Diff,
|
||||
Summary: impact.Summary,
|
||||
})
|
||||
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, RespApply{
|
||||
Sources: append([]string{}, impact.Sources...), // guarantee non nil slice
|
||||
StackID: impact.StackID.String(),
|
||||
Diff: impact.Diff,
|
||||
Summary: impact.Summary,
|
||||
Errors: convertParseErr(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, &influxdb.Error{
|
||||
Msg: fmt.Sprintf("unable to marshal; Err: %v", err),
|
||||
Code: influxdb.EInternal,
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type encoder interface {
|
||||
Encode(interface{}) error
|
||||
}
|
||||
|
||||
func formatSources(sources []string) string {
|
||||
return strings.Join(sources, "; ")
|
||||
}
|
||||
|
||||
func decodeWithEncoding(r *http.Request, v interface{}) (Encoding, error) {
|
||||
encoding := pkgEncoding(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 pkgEncoding(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
|
||||
}
|
||||
}
|
||||
|
||||
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) *influxdb.Error {
|
||||
return &influxdb.Error{
|
||||
Msg: fmt.Sprintf("unable to unmarshal %s", encoding),
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,507 @@
|
|||
package pkger_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/mock"
|
||||
"github.com/influxdata/influxdb/v2/pkg/testttp"
|
||||
"github.com/influxdata/influxdb/v2/pkger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestPkgerHTTPServerTemplate(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := ioutil.ReadFile(strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
})
|
||||
filesvr := httptest.NewServer(mux)
|
||||
defer filesvr.Close()
|
||||
|
||||
newPkgURL := func(t *testing.T, svrURL string, pkgPath string) string {
|
||||
t.Helper()
|
||||
|
||||
u, err := url.Parse(svrURL)
|
||||
require.NoError(t, err)
|
||||
u.Path = path.Join(u.Path, pkgPath)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
strPtr := func(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
t.Run("create pkg", func(t *testing.T) {
|
||||
t.Run("should successfully return with valid req body", func(t *testing.T) {
|
||||
fakeLabelSVC := mock.NewLabelService()
|
||||
fakeLabelSVC.FindLabelByIDFn = func(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
|
||||
return &influxdb.Label{
|
||||
ID: id,
|
||||
}, nil
|
||||
}
|
||||
svc := pkger.NewService(pkger.WithLabelSVC(fakeLabelSVC))
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/templates/export", pkger.ReqExport{
|
||||
Resources: []pkger.ResourceToClone{
|
||||
{
|
||||
Kind: pkger.KindLabel,
|
||||
ID: 1,
|
||||
Name: "new name",
|
||||
},
|
||||
},
|
||||
}).
|
||||
Headers("Content-Type", "application/json").
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
pkg, err := pkger.Parse(pkger.EncodingJSON, pkger.FromReader(buf))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, pkg)
|
||||
require.NoError(t, pkg.Validate())
|
||||
|
||||
assert.Len(t, pkg.Objects, 1)
|
||||
assert.Len(t, pkg.Summary().Labels, 1)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("should be invalid if not org ids or resources provided", func(t *testing.T) {
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), nil)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/templates/export", pkger.ReqExport{}).
|
||||
Headers("Content-Type", "application/json").
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusUnprocessableEntity)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("dry run pkg", func(t *testing.T) {
|
||||
t.Run("json", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType string
|
||||
reqBody pkger.ReqApply
|
||||
}{
|
||||
{
|
||||
name: "app json",
|
||||
contentType: "application/json",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
RawTemplate: bucketPkgKinds(t, pkger.EncodingJSON),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "defaults json when no content type",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
RawTemplate: bucketPkgKinds(t, pkger.EncodingJSON),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "retrieves package from a URL",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
Remotes: []pkger.ReqTemplateRemote{{
|
||||
URL: newPkgURL(t, filesvr.URL, "testdata/remote_bucket.json"),
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app jsonnet",
|
||||
contentType: "application/x-jsonnet",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
RawTemplate: bucketPkgKinds(t, pkger.EncodingJsonnet),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
dryRunFn: func(ctx context.Context, orgID, userID influxdb.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) {
|
||||
var opt pkger.ApplyOpt
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
pkg, err := pkger.Combine(opt.Pkgs)
|
||||
if err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
|
||||
if err := pkg.Validate(); err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
sum := pkg.Summary()
|
||||
var diff pkger.Diff
|
||||
for _, b := range sum.Buckets {
|
||||
diff.Buckets = append(diff.Buckets, pkger.DiffBucket{
|
||||
DiffIdentifier: pkger.DiffIdentifier{
|
||||
PkgName: b.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
return pkger.ImpactSummary{
|
||||
Summary: sum,
|
||||
Diff: diff,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/templates/apply", tt.reqBody).
|
||||
Headers("Content-Type", tt.contentType).
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespApply
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.Len(t, resp.Summary.Buckets, 1)
|
||||
assert.Len(t, resp.Diff.Buckets, 1)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("yml", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType string
|
||||
}{
|
||||
{
|
||||
name: "app yml",
|
||||
contentType: "application/x-yaml",
|
||||
},
|
||||
{
|
||||
name: "text yml",
|
||||
contentType: "text/yml",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
dryRunFn: func(ctx context.Context, orgID, userID influxdb.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) {
|
||||
var opt pkger.ApplyOpt
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
pkg, err := pkger.Combine(opt.Pkgs)
|
||||
if err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
|
||||
if err := pkg.Validate(); err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
sum := pkg.Summary()
|
||||
var diff pkger.Diff
|
||||
for _, b := range sum.Buckets {
|
||||
diff.Buckets = append(diff.Buckets, pkger.DiffBucket{
|
||||
DiffIdentifier: pkger.DiffIdentifier{
|
||||
PkgName: b.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
return pkger.ImpactSummary{
|
||||
Diff: diff,
|
||||
Summary: sum,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
body := newReqApplyYMLBody(t, influxdb.ID(9000), true)
|
||||
|
||||
testttp.
|
||||
Post(t, "/api/v2/templates/apply", body).
|
||||
Headers("Content-Type", tt.contentType).
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespApply
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.Len(t, resp.Summary.Buckets, 1)
|
||||
assert.Len(t, resp.Diff.Buckets, 1)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with multiple pkgs", func(t *testing.T) {
|
||||
newBktPkg := func(t *testing.T, bktName string) pkger.ReqRawTemplate {
|
||||
t.Helper()
|
||||
|
||||
pkgStr := fmt.Sprintf(`[
|
||||
{
|
||||
"apiVersion": "%[1]s",
|
||||
"kind": "Bucket",
|
||||
"metadata": {
|
||||
"name": %q
|
||||
},
|
||||
"spec": {}
|
||||
}
|
||||
]`, pkger.APIVersion, bktName)
|
||||
|
||||
pkg, err := pkger.Parse(pkger.EncodingJSON, pkger.FromString(pkgStr))
|
||||
require.NoError(t, err)
|
||||
|
||||
pkgBytes, err := pkg.Encode(pkger.EncodingJSON)
|
||||
require.NoError(t, err)
|
||||
return pkger.ReqRawTemplate{
|
||||
ContentType: pkger.EncodingJSON.String(),
|
||||
Sources: pkg.Sources(),
|
||||
Pkg: pkgBytes,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reqBody pkger.ReqApply
|
||||
expectedBkts []string
|
||||
}{
|
||||
{
|
||||
name: "retrieves package from a URL and raw pkgs",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
Remotes: []pkger.ReqTemplateRemote{{
|
||||
ContentType: "json",
|
||||
URL: newPkgURL(t, filesvr.URL, "testdata/remote_bucket.json"),
|
||||
}},
|
||||
RawTemplates: []pkger.ReqRawTemplate{
|
||||
newBktPkg(t, "bkt1"),
|
||||
newBktPkg(t, "bkt2"),
|
||||
newBktPkg(t, "bkt3"),
|
||||
},
|
||||
},
|
||||
expectedBkts: []string{"bkt1", "bkt2", "bkt3", "rucket-11"},
|
||||
},
|
||||
{
|
||||
name: "retrieves packages from raw single and list",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
RawTemplate: newBktPkg(t, "bkt4"),
|
||||
RawTemplates: []pkger.ReqRawTemplate{
|
||||
newBktPkg(t, "bkt1"),
|
||||
newBktPkg(t, "bkt2"),
|
||||
newBktPkg(t, "bkt3"),
|
||||
},
|
||||
},
|
||||
expectedBkts: []string{"bkt1", "bkt2", "bkt3", "bkt4"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
dryRunFn: func(ctx context.Context, orgID, userID influxdb.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) {
|
||||
var opt pkger.ApplyOpt
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
pkg, err := pkger.Combine(opt.Pkgs)
|
||||
if err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
|
||||
if err := pkg.Validate(); err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
sum := pkg.Summary()
|
||||
var diff pkger.Diff
|
||||
for _, b := range sum.Buckets {
|
||||
diff.Buckets = append(diff.Buckets, pkger.DiffBucket{
|
||||
DiffIdentifier: pkger.DiffIdentifier{
|
||||
PkgName: b.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return pkger.ImpactSummary{
|
||||
Diff: diff,
|
||||
Summary: sum,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/templates/apply", tt.reqBody).
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusOK).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespApply
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
require.Len(t, resp.Summary.Buckets, len(tt.expectedBkts))
|
||||
for i, expected := range tt.expectedBkts {
|
||||
assert.Equal(t, expected, resp.Summary.Buckets[i].Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validation failures", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType string
|
||||
reqBody pkger.ReqApply
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "invalid org id",
|
||||
contentType: "application/json",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: "bad org id",
|
||||
RawTemplate: bucketPkgKinds(t, pkger.EncodingJSON),
|
||||
},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid stack id",
|
||||
contentType: "application/json",
|
||||
reqBody: pkger.ReqApply{
|
||||
DryRun: true,
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
StackID: strPtr("invalid stack id"),
|
||||
RawTemplate: bucketPkgKinds(t, pkger.EncodingJSON),
|
||||
},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
fn := func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
dryRunFn: func(ctx context.Context, orgID, userID influxdb.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) {
|
||||
var opt pkger.ApplyOpt
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
pkg, err := pkger.Combine(opt.Pkgs)
|
||||
if err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
return pkger.ImpactSummary{
|
||||
Summary: pkg.Summary(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/templates/apply", tt.reqBody).
|
||||
Headers("Content-Type", tt.contentType).
|
||||
Do(svr).
|
||||
ExpectStatus(tt.expectedStatusCode)
|
||||
}
|
||||
|
||||
t.Run(tt.name, fn)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("apply a pkg", func(t *testing.T) {
|
||||
svc := &fakeSVC{
|
||||
applyFn: func(ctx context.Context, orgID, userID influxdb.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) {
|
||||
var opt pkger.ApplyOpt
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
pkg, err := pkger.Combine(opt.Pkgs)
|
||||
if err != nil {
|
||||
return pkger.ImpactSummary{}, err
|
||||
}
|
||||
|
||||
sum := pkg.Summary()
|
||||
|
||||
var diff pkger.Diff
|
||||
for _, b := range sum.Buckets {
|
||||
diff.Buckets = append(diff.Buckets, pkger.DiffBucket{
|
||||
DiffIdentifier: pkger.DiffIdentifier{
|
||||
PkgName: b.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
for key := range opt.MissingSecrets {
|
||||
sum.MissingSecrets = append(sum.MissingSecrets, key)
|
||||
}
|
||||
|
||||
return pkger.ImpactSummary{
|
||||
Diff: diff,
|
||||
Summary: sum,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
|
||||
svr := newMountedHandler(pkgHandler, 1)
|
||||
|
||||
testttp.
|
||||
PostJSON(t, "/api/v2/templates/apply", pkger.ReqApply{
|
||||
OrgID: influxdb.ID(9000).String(),
|
||||
Secrets: map[string]string{"secret1": "val1"},
|
||||
RawTemplate: bucketPkgKinds(t, pkger.EncodingJSON),
|
||||
}).
|
||||
Do(svr).
|
||||
ExpectStatus(http.StatusCreated).
|
||||
ExpectBody(func(buf *bytes.Buffer) {
|
||||
var resp pkger.RespApply
|
||||
decodeBody(t, buf, &resp)
|
||||
|
||||
assert.Len(t, resp.Summary.Buckets, 1)
|
||||
assert.Len(t, resp.Diff.Buckets, 1)
|
||||
assert.Equal(t, []string{"secret1"}, resp.Summary.MissingSecrets)
|
||||
assert.Nil(t, resp.Errors)
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue