mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
In this commit, the Directive is mocked; it doesn't actually reach out
to a controller.
Limits key translation to only those partitions (per index) specified in
the Directive. Attempting to create or find a key (or ID) for a
partition which is not handled by this node will result in an error;
translation requests are no longer forwarded to other nodes.
Limits import into only those shards specified, per index, by the
Directive. Attempting to import into a shard which is not handled by
this node will result in an error.
Stub out /directive endpoint
The `applyDirective()` method still needs to be implemented.
Update mds references to use the new /mds/types structure
In mds, we moved the shared types to mds/types. FeatureBase needs to
reference those instead.
This also bumps the mds version in go.mod.
Implement the Add/Remove Index part of Holder.ApplyDirective()
This adds functionality to `Holder.ApplyDirective()` which adds or
removes indexes (tables) based on those provided in the Directive. Still
to be implemented here: shards and partitions.
WIP: remove client from Batch
Move Batch into its own package: batch
Also, in order to avoid import loops, this introduces packages:
/batch/types
/client/types
Reorganize the Importer-related code
Moved the Importer interface to package: batch
Move the "pilosa client" implementation of the Importer interface to
package: client
Modify batch.NewBatch to take an Importer (not client)
This commit modifies the batch.NewBatch() function to use a functinal
option on Batch to inject an Importer into the Batch. Prior to this,
NewBatch() took a pointer to a client, which was a little too
restrictive. Now, MDS can implement an Importer which uses information
from MDS to determine to which node(s) the import calls should be directed.
Add client.SetAuthToken() method to satisfy SchemaManager interface
Update ApplyDirective logic to include fields.
This needs more work, but it was enough to get a basic test passing.
Move Transaction type into /types package.
Add interface check on batch.Importer no-op implementation
Updated ApplyDirective to create all currently support Field types
There are still the following TODOs:
- [ ] impolement field options (ex: decimal scale, int min/max, etc).
- [ ] `time` fields
Added support for Decimal.Scale in ApplyDirective
Update mds dependency
Add /health endpoint
Update to use dax (dax/mds) instead of mds.
After moving the mds repository into the dax repository as a
sub-package, this commit changes everything in FeatureBase to use the
dax repo instead of the now abandoned mds repo.
Introduce and implment the WriteLogger interfaces.
This adds both a `WriteLogReader` and `WriteLogWriter` interface. They
are both implemented by the implementation: `fileWriteLogger`. The
`fileWriteLogger` uses the dax/writelogger API to append log messages to
files on disk.
Add WriteLogWriter.ImportRoaring method to interface
This commit adds the `ImportRoaring` method to the `WriteLogWriter`
interface. Still to implement are the `Import` and `ImportValue`
methods.
Reorganize the ApplyDirective code
The primary goal was to cache the incoming Directive on the Holder prior
to applying all of the changes in the directive (i.e. loading data from
the WriteLogger) because applying those changes often validated against
the accepted state of the node.
Implement all of the WriteLogger read/write methods
Implement the HTTP WriteLogger implementation
WIP: Introduce shard.Version. Implement snapshotter.
Add HTTP Snapshotter implementation
This also recofigures server to use the HTTPSnapshotter instead of the
FileSnapshotter.
Implement snapshotter: TableKeys
Implement snapshotter: FieldKeys
Dependency dance
last of the dependency dance
Add support for prototype
This adds the Makefile targets to build the docker container and push it
to ECR.
SQL3 changes which break with dax changes
Missed TODO: implement FieldVersion version to WriteLogger
Address bug causing missing TranslateStores to error
Originally, we tried to limit the TranslateStores which get allocated to
only those for which the node is responsible. This works when adding a
new table. But if a table already exists, there's no logic to start
missing TranslateStores.
This reverts back to the old FeatureBase logic which brutishly allocates
a TranslateStore for every partition, even if one is not needed.
We need to address this by allowing the ApplyDirective logic to
initialize TranslateStores when they don't yet exist.
Move the ImportRoaringShardRequest type to the types package
Since the ImportRoaringShardRequest object is part of the Importer
interface, we need to move it to a non-root (i.e. pilosa) package. All
the other interface types are either concrete types or part of a
sub-package (such as roaring). We do this to prevent an implementer of
the interface from having to import the entire pilosa package and risk
circular imports.
buncha changes to support latest dax stuff
Move dax related types to /dax sub-package
This commit moves all the common "dax" types into the /dax sub-package.
The idea is to ensure that featurebase does not import dax at all.
It's ok if dax imports featurebase.
In the future, we might need to split the dax sub-package (common data
types used by muliple molecula data-plan services) into it's own repo.
Add type: dax.Schema
This isn't currently being used; I started to use is as a replacement
for pilosa_client.Schema, but then deferred that. But we'll need to do
it eventually, so it doesn't hurt to have this type in place.
Export RowIDs.Merge() method for use in orchestrator.
Add CreateSQL method to dax.Table type
The CreateSQL() method will return the "CREATE TABLE" statement required
to create the dax.Table.
Comment out confusing writelogger log message.
We need to revisit this, but for now, this log message is confusing.
Also, rename daxSharder to versionStore.
Remove hard-coded AWS account
Implement more FieldOptions such as Epoch
Some of the FieldOption logic was stubbed out in the dax package. This
commit fills that out more; specifically, it adds the
dax.Field.Options.Epoch parameter.
export stuff needed for TopK in orchestrator
export ValCount stuff to implement Percentile in orchestrator
export more stuff to support less code in orchestrator, shared objs
Port dax repo over to featurebase/dax (run all as sub-services)
This commit does ALOT. Sorry.
It introduces a `featurebase dax` sub-command which can be configured to
run the various dax services as sub-services within the same process, or
individually as the lone service in process.
It also changes all the URL paths to be prefixed with the service name.
So for example, instead of calling localhost:8080/status, you would now
call localhost:8080/featurebase/status.
Also, note that all services provide a /health endpoint to confirm they
are running in process.
Clean up integration tests. Remove PILOSA_ config prefix.
Remove duplicate clients (mistake from porting dax to featurebase)
Rename sub-service "featurebase" to "computer"
In the places where we have hard-coded the sub-service name into a URI
path, I've tried to tag the line with a comment containing:
`// #SERVICEPATHPREFIX`
Update copilot manifest files to reference "computer"
Port dax/README.md from dax repository
Separate (toml) Queryer Config from Injections
We needed to separate the toml config from the configuration required to
inject sub-services into the Queryer. I'm not sure this is the best
solution, but it's *a* solution. So here we are.
Clean up (i.e. remove) the queryer "implementations" package
Remove old test file
Run WriteLogger and Snapshotter as local sub-services.
Prior to this commit, the writelogger and snapshotter services only
worked when run as separate services. This allows them to be run in the
same process as all the other dax services.
There is still some naming issues that we should address, but it's
functional for now.
Clean up (i.e. organize) the intra-service interfaces.
Implement alpha Director for local messages from MDS to Computer
Prior to this commit, messages from MDS to the computer service were
still going over http. This commit introduces an interface
implementation which registers the local computer command, and use that
command's API to directly reference methods used by the Director.
Clean up a few more interface names
Add Queryer OpenAPI document.
Update copilot manifests to reflect latest changes
Add OpenAPI documents for WriteLogger and Snapshotter
Add OpenAPI document for MDS service
Add OpenAPI document for Computer service
Consolidate errors to use fb/errors package.
This commit is a first pass at trying to ensure that all of the DAX code
uses:
"github.com/molecula/featurebase/v3/errors"
This package is a wrapper for "github.com/pkg/errors", so going forward
we want to avoid importing that package.
The only method which isn't backward-compatible is `New()`; the
New() method in the featurebase/errors package takes an errors.Code. If
this becomes a problem, we could change this by reverting New() and then
introducing something like NewCoded(). But for now I think it might
actually discourage someone from just creating a New() error without
thinking about how it should be coded.
Introduce VersionStore interface
Move the existing VersionStore code to the `inmem` package as the
in-memory implementation of the new dax.VersionStore interface.
Introduce NodeService interface
With this, the Controller can maintain a registry of nodes by using this
NodeService interface as opposed to an in-memory map of nodes on the
Controller struct.
This also adds an inmem implementation of the NodeService interface.
Introduce controller.Balancer interface
This moves the existing balancer package to controller/naive package.
The idea is to allow us to add a different Balancer implementation in
the future.
Introduce DirectiveVersion interface
This commit also includes *A LOT* of refactoring to use dax.Worker and
dax.Job types everywhere instead of strings.
Introduce Schemar interface
The previous `Schemar` struct was moved to the `schemar/inmem` package,
and `Schemar` is now an interface implemented by that inmem package.
Remove unused type `nUnit`
Add boltdb implementation of VersionStore interface.
This removed the previous sqlite implementation; we decided not to use
sqlite for now (as a basic, local disk implementation) because it
requires CGO.
--------------------------------------------
No longer applicable:
Add sqlite implementation of VersionStore interface.
This commit implements the VersionStore interface using sqlite. Sqlite
requires CGO, so this may not be something we want to include, but it's
implemented here to get a feel for how an external implementation might
be used; the next step will be to determine how the user configured
FeatureBase to run using sqlite as a backing store for services like
MDS.
Add boltdb implementation of NodeService and DirectiveVersion interfaces.
Add boltdb implementation of naive Balancer interfaces.
This includes the two interfaces defined in `naive/balancer.go`:
- WorkerJobService
- FreeJobService
Add boltdb implementation of Schemar interface.
clean up a linter issue
Thread context.Context through all the interfaces.
Some of the interface implementations are going to use context, so we
need to make that part of the interface. The boltdb implementations, for
example, take a context. This is probably so we can do things like
cancel or timeout operations.
Update interfaces to return error; remove `panic(err)` everywhere.
Down-rev grpc version to 1.38.0
Later versions (after 1.42.0?) cause MustRunCluster.Close() in tests to
deadlock.
This commit also adds an `isComputeNode` feature flag around some of the
write log and shard/partition check functionality so that it doesn't run
under normal conditions (this is excercised by running the sql3 tests
for example).
Add MDS_Persistence test to cover meta data persistence
This adds a basic test which configures the MDS container to use boltdb
as its persistence storage, saved on a docker volume. Then, the mds
container is stopped/replaced, and we confirm that the data stored on
the volume is availble to the new MDS container.
Fix a few things after rebase with sql-experiment branch
The lastest version of sql-experiment contains a fairly significan
refactor of the way query iteration works. This commit adjusts for those
changes.
pull dax IDK changes in to FB IDK (#2177)
* pull dax IDK changes in to FB IDK
* Move docker-related IDK build stuff to featurebase root
Building the docker image required the root level go.mod and vendor
directory. This change moves the make targets to the root level
Makefile, and the Dockerfiles now copy the root level vendor directory
(and everything else in the root for that matter).
* Fix batch- and client-related tests
* InitializePoller on MDS restart/replacement
Prior to this change, if MDS was restarted, its internal poller (which
maintains an in-memory list of nodes to poll) is empty. This is bad,
because it doesn't know about nodes that it should be polling.
This change fixes that. Upon MDS startup, it intializes the poller with
the list of nodes that MDS keeps in persistent storage (currently:
boltdb).
* Add EFS volume to MDS Copilot manifest
This allows us to use MDS's persistent storage (via boltdb) in the
Copilot demo by saving metadata in a boltdb file on EFS.
* Thread logger.Logger through all dax components
* Revert some of the breaking changes from DAX development.
When we first started prototyping DAX, we made changes to the
featurebase core code which would have broken the existing featurebase
functionality. This commit reverts some of those changes. Anywhere that
we need to modify core featurebase functionilty, we put it behind some
kind of feature flag. This flag is typically determined by whether the
running node is a "compute" node (i.e. DAX.COMPUTER.RUN = true).
Co-authored-by: Travis Turner <travis@molecula.com>
add packaging for DAX
need cgo for datagen build
bind to 0.0.0.0, pass GOOS and GOARCH explicitly
not sure if the explicit GOOS/GOARCH is actually necessary...
Get INSERT INTO (aka ingest) working through SQL3
This commit does a few things which I'll try do describe here.
- Introduces a Qctx interface. The existing Qcx is an implementation of
this interface, and can be used exactly how it has been. But this
allows us to abstract away the notion of Qcx in the Queryer (which is
handling SQL3) until we're ready to address that. As an example, the
Qcx has a notion of a featurebase Holder, but that doesn't make sense
when we're at the Queryer layer. For now, the Qctx used in the Queryer
is a no-op.
- Adds a ComputeAPI interface implementation for the Queryer. This is
effectively the Import() and ImportValues() methods used for ingest.
The logic here handles the incoming ImportRequest by first doing any
necessary column and row translation for the entire request, then it
splits the records by shard, and generates a new ImportRequest per
shard with only the shard-appropriate records.
- Changes the mds.Importer to take an MDS interface implementation
(which can be an mds client) instead of an mdsAddress. This allows us
to use a localy MDS implementation rather than assuming we need a
client to make calls over a network.
Add queryer.Importer interface to handle ingest via SQL (#2203)
* Add queryer.Importer interface to handle ingest via SQL
This is meant to support ingest through SQL when the queryer and the
compute services are running in the same process, or when they are on
seperate processes and need to talk via http client.
* remove datagen from RPM
was originally added as a convenience to generate test data, but is
unused and annoying because datagen doesn't easily cross-compile due
to cgo
* add marshalUnmarshal to controller to avoid passing pointers
passing pointers across API boundaries can cause unpredictable things
in local vs remote configurations.
Co-authored-by: Matthew Jaffee <jaffee@pilosa.com>
"fix" a few issues with wrong default partition numbers
these still need to be properly fixed and actually get the correct
data from MDS
go mod tidy
Introduce TableQualifier (OrganizationID/DatabaseID) (#2220)
* add check in ApplyDirective that version is increasing
fix TestAPIDirective to make version always increasing
* fix docker image build and break out dax test in CI
We have to run the DAX integration tests separately as they call out
to Docker, and so it isn't easy to run them in a Docker container as
the other tests do. So we run them directly on the CI runner which has
Docker and Go installed.
We also explicitly exclude these tests from running during the other
tests.
Also my editor was automatically reformatting some comments badly
which is why I added the "data" thing in those two places
* add timeout to poller
* give Poller a default Logger
apparently we can NPE sometimes, seen in CI: https://gitlab.com/molecula/featurebase/-/jobs/3028286364
* bunch of testing fixes, mostly IDK/DAX related
make MDS error if sendDirectives errors, don't just
log. sendDirectives can error if computer nodes disagree about the
validity of a schema (for example), in which case it might need to get
deleted and user notified somehow. very messy, needs more thought.
re-introduce old env prefix to maintain compatibility with master
branch
make self-contained dax container for IDK testing
build IDK images from source (now that all the source is available
since it's in the same repo)
catch errors in DoExtractQuery in idktest.go
fix IDK bug where prefix path was hardcoded in all cases rather than
only when useMDS was true
fix TestBatchTargetMDS... needed to add field options and catch error
when creating table. also needed an _id field
* fix env prefix in tests
* WIP getting tests to pass, wanna see CI
* don't error if we get a zero version directive and we don't have a
directive yet
* cleanup debugging junk
* "fix" future.rename thing, run IDK tests
* Introduce TableQualifier (OrganizationID/DatabaseID)
This commit introduces a lot of new types (in dax/table.go) related to
TableQualifer (which is made up of OrganizationID and DatabaseID), as
well as things like TableID and TableKey.
For the most part, we try to thread a QualifiedTableID through the
entirety of DAX. There are some places (for example in the Balancers,
which are just aware of string keys) which use a string TableKey
(tbl__org__db__tableid).
* Remove some debugging comments
* Add Org/DB support to CLI.
This commit adds support for special commands:
SET
SET ORG acme
SET DB db1
USE db1
* remove ".pulled" from IDK Makefile
I don't think we need it any more as most things can be built
locally. I think it was only there to refresh the FeatureBase images
that were tagged as master, but we don't need to do that any more.
* Change DAX json tags to kebab-case (i.e. hyphenated)
This commit also renames some struct arguments to more accurately
reflect their type: for example, renaming `Table` to `TableKey` when the
type is TableKey.
* Return DAX TableName in SHOW TABLES (instead of Index.Name)
There are cases where SchemaAPI is used to return DAX friendly table
names (as opposed to featurebase index names, which are DAX TableKey).
This is an attempt to do that. With that said, it's not ideal because
anything could call those API methods and expect the other type.
* Fix a bug which wasn't completely dropping a table.
When using boltdb as a backend, DROP TABLE wasn't removing the
reverse-lookup key for the table in boltdb.
* Remove idk/testenv/certs which got accidentally committed.
also update .gitignore to include those.
* Fix IDK ingest tests to be TableQualifier aware.
* Add example Table types to dax/table.com godoc.
* ignore idk.Main fields for flags, upgrade commandeer
* go mod tidy
* Fix DAX integration tests: ingester using wrong ENV VARs
We change from ORGANIZATION_ID to ORG_ID
and from DATABASE_ID to DB_ID
* Clarify things around idk (docker) tests
* Stop running TestKafkaSourceIntegration with t.Parallel()
This test can't be run in parallel as it's currently written. Doing so
allows for interleaving of messages to the same kafka topic between
tests.
I didn't attempt to modify the test so it could be run in parallel. That
could be done, but left for someone more ambitious.
Co-authored-by: Matthew Jaffee <jaffee@pilosa.com>
Require Directive.Version be a non-zero value. (#2227)
Because the directive cached on the holder is not a pointer, its default
version is 0. In order to avoid having to compare against that, we just
require that Directive.Version start at 1.
General, non-invasive code cleanup and comment adjustment.
Move ImportRoaringShardRequest out of the types package
Early on in the DAX development, I moved ImportRoaringShardRequest into
a types package. There must have been some import loop going on, but
since that is not longer the case, it's safe to move this back into the
core featurebase (er... pilosa) package.
Move Transaction struct back into the pilosa package (from types)
Revert some name changes (cli -> client)
Add DAX Handler CloseTimeout
This was implemented in htt_handler.go, but it had been commented out in
the DAX handler. This just uncomments that and finishes the
implementation.
Remove Qcx from queryer.Importer interface
This sets us up to revert the Qctx interface that was initially
introduced to allow us to abstract away the need for a Qcx when calling
the ComputeAPI from a remote service (i.e. the queryer).
Add some go-doc comments and remove unused code.
Move SchemaManager setup from datagen to idk.Main (#2233)
The set for idk.SchemaManager (for dax implementations) was previously
in datagen. This may have been because of some import loop problem
during development, but that's no longer an issue.
The setup for this should be in idk.Main so anything using that can
leverage the MDS-specific SchemaManager setup.
Fix issues around nil TxFactory
First, don't return a nil. Rather return a new *TxFactory (with no
holder).
Second, don't call `f.holder` in the testhook outside of checking if
`f.holder` is nil.
Wrap all bare errors
Make service prefixes constants
Instead of having `"computer"` throughout the code, use instead a
constant: `dax.ServicePrefixComputer`.
MDS skip errors when sending empty directives
also add in the docker-login and ecr-push changes for serverless DAX
Fix the logic in Directive.IsEmpty() (#2236)
Update the cached value for Index.translatePartitions
In the case where a node already knows about an index, but its
assignment of partitions for that index changes (for example, when
another node goes down and the node in question is now responsible for
more partitions than it previously was), then we need to update the
cached value of Index.translatePartitions because that's used in
translation checks.
minor fixes for IDK-related bugs
WIP: tokenize CLI to access cloud
FB CLI cloud support with automatic token refresh
Also adds support for a GET command which allows making HTTP GET
queries to cloud CP which can be handy for debugging stuff. E.g. GET /v2/databases
buncha little fixes working on writelogger stuff
fix writelogger/snapshotter setup bugs
implement writelogging for importRoaringShard
add debug endpoint to MDS
use shard transactional endpoint in MDS datagen
add debugging to API related to writelogger
revert handleroption change
clean up big PR
remove "GET" command from CLI for making arbitrary HTTP request to
cloud control plane (was a messy hack and not that useful)
remove json tags from FB objects where we had to duplicate the object
elsewhere due to import loops and weren't actually json encoding it
unexport handlerOption which was exported to try to avoid doing
certain things if we're in DAX mode, but I didn't end up merging that code.
remove (hopefully) unecessary extra call to api.indexField
fix some formatting, unexport some vars, godoc, etc
oops, fix build failure
Update FeatureBase CLI to support a standard deployment
The standard deployment uses a different endpoint and request payload.
This commit tries to detect is the standard deployment is being used,
and if so, it uses a standard-specific FBQueryer.
It also modifies the auto-detection logic to try standard featurebase
and dax ports in the case where a port was not provided.
MDS API refactor (#2259)
* MDS API refactor
table IDs are exposed but only created server side
also cleaned up dax Makefile
* clean up review feedback
Co-authored-by: Travis Turner <travis@pilosa.com>
* remove TablesByName
* rip out inmem implementations and use boltdb everywhere
* remove inmem balancer, create bolt tempfile by default on startup
* WIP on snapshot table impl and test
* Minor comment and code layout adjustments.
This commit also adds the `Equals` method to `QualifiedTableID` for
equality comparisons. It's no longer safe to compare struct (two structs
might still be equal even if one of the structs doesn't have a `Name`
value.
* Use a unique docker network for each dax test
Ocassionally we would see some test failures due to a network already
existing. This shouldn't happen, but to avoid that, this commit
generates a unique name for each sub test (which gets deleted at the end
of every test).
* Fix one instance of NewQualifiedTableID losing Name
We should probably check the other instances and see if Name is getting
lost.
* simplify unique network stuff and fix api directive tests
* Remove TableIDRequest and TableIDResponse types for /table-id (#2267)
For the mds/table-id http requests, just use dax.QualifiedTableID as
both the request and response types.
* remove lattice from dax, no error on node re-reg, dax docker-compose
* various updates
* WIP: mds-refactor branch review
* no-op on SnapshotTableKeys if table is not keyed
* Makefile helpers
* add doWeCare so controller doesn't fail unnecessarily
* clean up table creation (#2272)
* Strip underscores from TableID stub name
* fix boltdb versionstore tests: generate unique, sorted tables
* fix controller test related to reregistering a node
* JobSet -> generic Set
Co-authored-by: Travis Turner <travis@pilosa.com>
Co-authored-by: Travis Turner <travis@molecula.com>
Cleanup after rebase on master
The latest rebase on master entailed all the client/batch changes as
well as some of the qcx refactoring. It made for a hairy rebase. This
commit fixes some of the tests that were failing after that rebase.
Fix batch/client import loop missed during rebase (#2280)
It's not surprising that `batch` can't import `client`. It was doing
that here (importing an error type from the `client` package). What is
surprising is that it's okay for `batch_test.go` to import `client` even
though `batch_test.go` is an internal test and therefore part of the
`batch` package.
different boltDB's for schemar/controller, explicit balancers
nice helpers for dax docker-compose, make build really fast
build FB binary outside of docker, then create Docker image with its
working dir in an empty subdirectory so it doesn't send a GB of
context to the daemon.
error on unassigned jobs and use client with timeout
fix CR feedback
deregister batch of nodes
also make removal faster via director dial timeout
implement WorkersForJobPrefix so orchestrator doesn't make up shards
also fix some godocs and remove unused method
Run sub-tasks of a Directive concurrently in a worker pool. (#2275)
* Run sub-tasks of a Directive concurrently in a worker pool.
This allows the compute node to concurrently load shapshot and writelog
data concurrently, instead of one keyset/partition/shard at a time.
It introduces a config parameter called `DirectiveWorkerPoolSize`.
* code review cleanup
* Use unique container names in DAX integration tests
We were seeing "container already exists" errors in CI, so just to be
safe, this commit constructs a unique container name for every container
in the DAX integration test run.
Stub in SystemAPI to Queryer (note: will not work if used)
This just makes is so that dax can compile. Actually implementing
system-table functionality for dax will take some planning.
Tlt/dax merge prep (#2282)
* Remove copilot directory
* Remove Dockerfile-datagen-long
* Remove orphaned RegisterNodeRequest
This type is not defined in the dax/mds/http package.
* implement TIMEQUANTUM and TTL in Table.Field type
* Remove the "service" misdirection in queryer/writelogger/snapshotter.
We had originally used an additional layer, er.. package, for a "service".
The main distinction was that the Config differed in that it was
internal, unlike the Config that we need to provide for the top-level
server config (i.e. toml). Having that additional layer just to support
a different Config seemed premature at best. So I'm removing it.
* Remove dax docker containers no longer used in tests
Since we run everything as "featurebase", we don't have multiple
container types anymore.
* Some minor comment updates
* Remove nfpm stuff related to dax
* Fix linter issues
Fix "duplicate" issues raised by sonarcloud.
run docker components of dax integration tests with coverage
trying to get dax integration coverage
add coverate volume mounts throughout dax integration tests
add a lock, tweak dax Makefile, remote flag on query handler
remove some unused code
convert batch tests to use clustertests to get coverage
maybe fix clustertests
more authclustertests fixes, test is failing locally
but also seems to have been silently failing in CI prior to these
changes... let's see if it's still silent
fix some lint to kick CI
just re-running the job wasn't working... strange behavior
remove RetryLogic test and pipe which don't work
RetryLogic test removed due to etcd changes. Seebs thinks we shouldn't
test this here.
Pipe was being ignored since we're no longer using "bash -c" to
execute the command. If we need to generate that output file we'll
either have to reintroduce bash -c and set -o pipefail so that it
actually fails properly, or figure out some other solution.
shooting into the dark...
first cut at bulk node registration
remove unused stuff from batch tests, set coverpkg to ../...
batch registration timeout and fix tests
disable most tests and don't run fb background batch test
debuggin!!!!!!!!!
and then he tried this....
Implement importer (for INSERT INTO) in the Queryer
Prior to this, we we passing a nil value in for the importer to the
planner.NewExecutionPlanner in the Queryer. This meant that INSERT INTO
statements didn't work. Now they should.
It uses the importer that we build for IDK in /idk/mds/importer.go, and
wrapps that with a type that can determine if the provided string
"index" is of the form indexName or TableKey.
turn off debug mode, fix log saving
Run sql3 test definitions in a dax integration test
There are currently 22 tests which are not passing. They are skipped in
the "skips" slice.
WIP, not working, pql queries to tests
Add TableQualifier to PQL query logic in the Queryer
Add more PQL tests to the keyed table
Allow instant node registration if registration-batch-timeout=0
When running dax services in process, we don't want to wait 3s for the
compute node to register; we know it's there because it's in the same
process.
Fixes related to IncludesColumn PQL test.
Tests for ConstRow and FieldValue
cleanup
add UnionRows and Options, better error reporting on bad queries
delete unused schemar client.go, clean up unused in batch test CI
move test timeouts into more reasonable territory
apparently this had already been done, but got merge-stommped at some point
move dax bolt test helpers into dax package
Add computer CheckIn routine (#2296)
* Add computer CheckIn routine
This adds a background routine which sends a "check-in" request to MDS
every <interval>. This is to address the case where the poller has
removed a computer node from the node list (due to a network fault, for
example), but the node is still healthy and becomes available again. In
that case, the node needs to "check-in" to tell MDS it is still there.
MDS will likely send the node a new directive with Method=reset telling
the node to delete all of its data an apply the latest directive.
* Don't send directives to Deregistered (i.e. removed) nodes
We have an issue where we're locking on sendDirective in the
controller, and when the node is unavailable, the send hangs and never
releases the lock. This is a temporary fix for that until we address the
real problem.
Fix .gitlab-ci.yml after rebase
fix some indentation shenanigans
(cherry picked from commit 20a8b5713a)
4021 lines
124 KiB
Go
4021 lines
124 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package pilosa
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"expvar"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/big"
|
|
"mime"
|
|
"net"
|
|
"net/http"
|
|
_ "net/http/pprof" // Imported for its side-effect of registering pprof endpoints with the server.
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"runtime/debug"
|
|
"runtime/pprof"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/authn"
|
|
"github.com/featurebasedb/featurebase/v3/authz"
|
|
"github.com/featurebasedb/featurebase/v3/disco"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/featurebasedb/featurebase/v3/monitor"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
"github.com/featurebasedb/featurebase/v3/rbf"
|
|
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
|
"github.com/featurebasedb/featurebase/v3/storage"
|
|
"github.com/featurebasedb/featurebase/v3/tracing"
|
|
"github.com/felixge/fgprof"
|
|
"github.com/gorilla/handlers"
|
|
"github.com/gorilla/mux"
|
|
"github.com/molecula/featurebase/v3/authn"
|
|
"github.com/molecula/featurebase/v3/authz"
|
|
"github.com/molecula/featurebase/v3/dax"
|
|
"github.com/molecula/featurebase/v3/disco"
|
|
"github.com/molecula/featurebase/v3/logger"
|
|
"github.com/molecula/featurebase/v3/monitor"
|
|
"github.com/molecula/featurebase/v3/pql"
|
|
"github.com/molecula/featurebase/v3/rbf"
|
|
"github.com/molecula/featurebase/v3/sql3/planner/types"
|
|
"github.com/molecula/featurebase/v3/storage"
|
|
"github.com/molecula/featurebase/v3/tracing"
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/prometheus/prom2json"
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
const (
|
|
// HeaderRequestUserID is request userid header
|
|
HeaderRequestUserID = "X-Request-Userid"
|
|
)
|
|
|
|
// Handler represents an HTTP handler.
|
|
type Handler struct {
|
|
Handler http.Handler
|
|
|
|
fileSystem FileSystem
|
|
|
|
logger logger.Logger
|
|
|
|
queryLogger logger.Logger
|
|
|
|
// Keeps the query argument validators for each handler
|
|
validators map[string]*queryValidationSpec
|
|
|
|
api *API
|
|
|
|
ln net.Listener
|
|
// url is used to hold the advertise bind address for printing a log during startup.
|
|
url string
|
|
|
|
closeTimeout time.Duration
|
|
|
|
serializer Serializer
|
|
roaringSerializer Serializer
|
|
|
|
server *http.Server
|
|
|
|
middleware []func(http.Handler) http.Handler
|
|
|
|
pprofCPUProfileBuffer *bytes.Buffer
|
|
|
|
auth *authn.Auth
|
|
|
|
permissions *authz.GroupPermissions
|
|
|
|
// sqlEnabled is serving as a feature flag for turning on/off the /sql
|
|
// endpoint.
|
|
sqlEnabled bool
|
|
}
|
|
|
|
// externalPrefixFlag denotes endpoints that are intended to be exposed to clients.
|
|
// This is used for stats tagging.
|
|
var externalPrefixFlag = map[string]bool{
|
|
"schema": true,
|
|
"query": true,
|
|
"import": true,
|
|
"export": true,
|
|
"index": true,
|
|
"field": true,
|
|
"nodes": true,
|
|
"version": true,
|
|
}
|
|
|
|
const (
|
|
// OriginalIPHeader is the original IP for client
|
|
// It is used mainly for authenticating on remote nodes
|
|
// ForwardedIPHeader gets updated to the node's IP
|
|
// when requests are forward to other nodes in the cluster
|
|
OriginalIPHeader = "X-Molecula-Original-IP"
|
|
|
|
// ForwardedIPHeader is part of the standard header
|
|
// it is used to identify the originating IP of a client
|
|
ForwardedIPHeader = "X-Forwarded-For"
|
|
|
|
// AllowedNetworksGroupName is used for the admin group authorization
|
|
// when authentication is completed through checking the client IP
|
|
// against the allowed networks
|
|
AllowedNetworksGroupName = "allowed-networks"
|
|
)
|
|
|
|
type errorResponse struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// handlerOption is a functional option type for Handler
|
|
type handlerOption func(s *Handler) error
|
|
|
|
func OptHandlerMiddleware(middleware func(http.Handler) http.Handler) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.middleware = append(h.middleware, middleware)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerAllowedOrigins(origins []string) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.middleware = append(h.middleware, handlers.CORS(
|
|
handlers.AllowedOrigins(origins),
|
|
handlers.AllowedHeaders([]string{"Content-Type"}),
|
|
))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerAPI(api *API) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.api = api
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerAuthN(authn *authn.Auth) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.auth = authn
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerAuthZ(gp *authz.GroupPermissions) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.permissions = gp
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerFileSystem(fs FileSystem) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.fileSystem = fs
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerLogger(logger logger.Logger) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.logger = logger
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerQueryLogger(logger logger.Logger) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.queryLogger = logger
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerSerializer(s Serializer) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.serializer = s
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptHandlerRoaringSerializer(s Serializer) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.roaringSerializer = s
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// OptHandlerListener set the listener that will be used by the HTTP server.
|
|
// Url must be the advertised URL. It will be used to show a log to the user
|
|
// about where the Web UI is. This option is mandatory.
|
|
func OptHandlerListener(ln net.Listener, url string) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.ln = ln
|
|
h.url = url
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// OptHandlerCloseTimeout controls how long to wait for the http Server to
|
|
// shutdown cleanly before forcibly destroying it. Default is 30 seconds.
|
|
func OptHandlerCloseTimeout(d time.Duration) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.closeTimeout = d
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// OptHandlerSQLEnabled enables the /sql endpoint.
|
|
func OptHandlerSQLEnabled(v bool) handlerOption {
|
|
return func(h *Handler) error {
|
|
h.sqlEnabled = v
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var makeImportOk sync.Once
|
|
var importOk []byte
|
|
|
|
// NewHandler returns a new instance of Handler with a default logger.
|
|
func NewHandler(opts ...handlerOption) (*Handler, error) {
|
|
handler := &Handler{
|
|
fileSystem: NopFileSystem,
|
|
logger: logger.NopLogger,
|
|
closeTimeout: time.Second * 30,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
err := opt(handler)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "applying option")
|
|
}
|
|
}
|
|
if handler.serializer == nil || handler.roaringSerializer == nil {
|
|
return nil, errors.New("must use serializer options when creating handler")
|
|
}
|
|
makeImportOk.Do(func() {
|
|
var err error
|
|
importOk, err = handler.serializer.Marshal(&ImportResponse{Err: ""})
|
|
if err != nil {
|
|
panic(fmt.Sprintf("trying to cache import-OK response: %v", err))
|
|
}
|
|
})
|
|
|
|
// if OptHandlerFileSystem is used, it must be before newRouter is called
|
|
handler.Handler = newRouter(handler)
|
|
handler.populateValidators()
|
|
|
|
if handler.api == nil {
|
|
return nil, errors.New("must pass OptHandlerAPI")
|
|
}
|
|
|
|
if handler.ln == nil {
|
|
return nil, errors.New("must pass OptHandlerListener")
|
|
}
|
|
|
|
handler.server = &http.Server{Handler: handler}
|
|
|
|
return handler, nil
|
|
}
|
|
|
|
func (h *Handler) Serve() error {
|
|
err := h.server.Serve(h.ln)
|
|
if err != nil && err.Error() != "http: Server closed" {
|
|
h.logger.Errorf("HTTP handler terminated with error: %s\n", err)
|
|
return errors.Wrap(err, "serve http")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close tries to cleanly shutdown the HTTP server, and failing that, after a
|
|
// timeout, calls Server.Close.
|
|
func (h *Handler) Close() error {
|
|
deadlineCtx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(h.closeTimeout))
|
|
defer cancelFunc()
|
|
err := h.server.Shutdown(deadlineCtx)
|
|
if err != nil {
|
|
err = h.server.Close()
|
|
}
|
|
return errors.Wrap(err, "shutdown/close http server")
|
|
}
|
|
|
|
func (h *Handler) populateValidators() {
|
|
h.validators = map[string]*queryValidationSpec{}
|
|
h.validators["GetExport"] = queryValidationSpecRequired("index", "field", "shard")
|
|
h.validators["GetIndexes"] = queryValidationSpecRequired()
|
|
h.validators["GetIndex"] = queryValidationSpecRequired()
|
|
h.validators["PostIndex"] = queryValidationSpecRequired()
|
|
h.validators["DeleteIndex"] = queryValidationSpecRequired()
|
|
h.validators["GetTranslateData"] = queryValidationSpecRequired("index").Optional("partition", "field")
|
|
h.validators["PostTranslateKeys"] = queryValidationSpecRequired()
|
|
h.validators["PostField"] = queryValidationSpecRequired()
|
|
h.validators["DeleteField"] = queryValidationSpecRequired()
|
|
h.validators["PostImport"] = queryValidationSpecRequired().Optional("clear", "ignoreKeyCheck")
|
|
h.validators["PostImportAtomicRecord"] = queryValidationSpecRequired().Optional("simPowerLossAfter")
|
|
h.validators["PostImportRoaring"] = queryValidationSpecRequired().Optional("remote", "clear")
|
|
h.validators["PostQuery"] = queryValidationSpecRequired().Optional("shards", "excludeColumns", "profile", "remote")
|
|
h.validators["GetInfo"] = queryValidationSpecRequired()
|
|
h.validators["RecalculateCaches"] = queryValidationSpecRequired()
|
|
h.validators["GetSchema"] = queryValidationSpecRequired().Optional("views")
|
|
h.validators["PostSchema"] = queryValidationSpecRequired().Optional("remote")
|
|
h.validators["GetStatus"] = queryValidationSpecRequired()
|
|
h.validators["GetVersion"] = queryValidationSpecRequired()
|
|
h.validators["PostClusterMessage"] = queryValidationSpecRequired()
|
|
h.validators["GetFragmentBlockData"] = queryValidationSpecRequired()
|
|
h.validators["GetFragmentBlocks"] = queryValidationSpecRequired("index", "field", "view", "shard")
|
|
h.validators["GetFragmentData"] = queryValidationSpecRequired("index", "field", "view", "shard")
|
|
h.validators["GetFragmentNodes"] = queryValidationSpecRequired("shard", "index")
|
|
h.validators["GetPartitionNodes"] = queryValidationSpecRequired("partition")
|
|
h.validators["GetNodes"] = queryValidationSpecRequired()
|
|
h.validators["GetShardMax"] = queryValidationSpecRequired()
|
|
h.validators["GetTransactionList"] = queryValidationSpecRequired()
|
|
h.validators["GetTransactions"] = queryValidationSpecRequired()
|
|
h.validators["GetTransaction"] = queryValidationSpecRequired()
|
|
h.validators["PostTransaction"] = queryValidationSpecRequired()
|
|
h.validators["PostFinishTransaction"] = queryValidationSpecRequired()
|
|
|
|
}
|
|
|
|
type contextKeyQuery int
|
|
|
|
const (
|
|
contextKeyQueryRequest contextKeyQuery = iota
|
|
contextKeyQueryError
|
|
contextKeyGroupMembership
|
|
)
|
|
|
|
// addQueryContext puts the results of handler.readQueryRequest into the Context for use by
|
|
// both other middleware and any handlers.
|
|
func (h *Handler) addQueryContext(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
pathParts := strings.Split(r.URL.Path, "/")
|
|
if len(pathParts) > 3 && pathParts[3] == "query" {
|
|
req, err := h.readQueryRequest(r)
|
|
ctx := context.WithValue(r.Context(), contextKeyQueryRequest, req)
|
|
ctx = context.WithValue(ctx, contextKeyQueryError, err)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
} else {
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (h *Handler) queryArgValidator(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
key := mux.CurrentRoute(r).GetName()
|
|
|
|
if validator, ok := h.validators[key]; ok {
|
|
if err := validator.validate(r.URL.Query()); err != nil {
|
|
errText := err.Error()
|
|
if validHeaderAcceptJSON(r.Header) {
|
|
response := errorResponse{Error: errText}
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
h.logger.Errorf("failed to encode error %q as JSON: %v", errText, err)
|
|
} else {
|
|
errText = string(data)
|
|
}
|
|
}
|
|
http.Error(w, errText, http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (h *Handler) extractTracing(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
span, ctx := tracing.GlobalTracer.ExtractHTTPHeaders(r)
|
|
defer span.Finish()
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func (h *Handler) collectStats(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t := time.Now()
|
|
next.ServeHTTP(w, r)
|
|
dur := time.Since(t)
|
|
|
|
statsTags := make([]string, 0, 5)
|
|
|
|
longQueryTime := h.api.LongQueryTime()
|
|
if longQueryTime > 0 && dur > longQueryTime {
|
|
queryRequest := r.Context().Value(contextKeyQueryRequest)
|
|
|
|
var queryString string
|
|
if req, ok := queryRequest.(*QueryRequest); ok {
|
|
queryString = req.Query
|
|
}
|
|
|
|
h.logger.Printf("HTTP query duration %v exceeds %v: %s %s %s", dur, longQueryTime, r.Method, r.URL.String(), queryString)
|
|
statsTags = append(statsTags, "slow:true")
|
|
} else {
|
|
statsTags = append(statsTags, "slow:false")
|
|
}
|
|
|
|
pathParts := strings.Split(r.URL.Path, "/")
|
|
if externalPrefixFlag[pathParts[1]] {
|
|
statsTags = append(statsTags, "where:external")
|
|
} else {
|
|
statsTags = append(statsTags, "where:internal")
|
|
}
|
|
|
|
statsTags = append(statsTags, "useragent:"+r.UserAgent())
|
|
|
|
path, err := mux.CurrentRoute(r).GetPathTemplate()
|
|
if err == nil {
|
|
statsTags = append(statsTags, "path:"+path)
|
|
}
|
|
|
|
statsTags = append(statsTags, "method:"+r.Method)
|
|
|
|
stats := h.api.StatsWithTags(statsTags)
|
|
if stats != nil {
|
|
stats.Timing(MetricHTTPRequest, dur, 0.1)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (h *Handler) monitorPerformance(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !monitor.IsOn() {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
// %% begin sonarcloud ignore %%
|
|
prefixes := make(map[string]struct{})
|
|
prefixes["index"] = struct{}{}
|
|
prefixes["info"] = struct{}{}
|
|
prefixes["schema"] = struct{}{}
|
|
prefixes["status"] = struct{}{}
|
|
prefixes["queries"] = struct{}{}
|
|
prefixes["query-history"] = struct{}{}
|
|
|
|
pathParts := strings.Split(r.URL.Path, "/")
|
|
// checks < 5 to exclude import endpoints for now from sentry transactions as
|
|
// there could potentially be many of them. Pricing is per transaction.
|
|
if len(pathParts) > 1 && len(pathParts) < 5 {
|
|
if _, ok := prefixes[pathParts[1]]; ok {
|
|
path := scrubPath(pathParts)
|
|
span := monitor.StartSpan(r.Context(), "HTTP", path)
|
|
qreq := r.Context().Value(contextKeyQueryRequest)
|
|
req, ok := qreq.(*QueryRequest)
|
|
if ok && req != nil {
|
|
span.SetTag("PQL Query", req.Query)
|
|
span.SetTag("SQL Query", req.SQLQuery)
|
|
span.SetTag("Index", mux.Vars(r)["index"])
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
span.Finish()
|
|
return
|
|
}
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
// %% end sonarcloud ignore %%
|
|
})
|
|
}
|
|
|
|
func scrubPath(pathParts []string) string {
|
|
l := len(pathParts)
|
|
if l > 1 && pathParts[1] == "index" {
|
|
switch {
|
|
case l > 4:
|
|
pathParts[4] = "{field}"
|
|
fallthrough
|
|
case l > 2:
|
|
pathParts[2] = "{index}"
|
|
}
|
|
}
|
|
return strings.Join(pathParts, "/")
|
|
}
|
|
|
|
// latticeRoutes lists the frontend routes that do not directly correspond to
|
|
// backend routes, and require special handling.
|
|
var latticeRoutes = []string{"/tables", "/query", "/querybuilder", "/signin"} // TODO somehow pull this from some metadata in the lattice directory
|
|
|
|
// newRouter creates a new mux http router.
|
|
func newRouter(handler *Handler) http.Handler {
|
|
router := mux.NewRouter()
|
|
|
|
// TODO: figure out how to protect these if needed
|
|
router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux).Methods("GET")
|
|
router.PathPrefix("/debug/fgprof").Handler(fgprof.Handler()).Methods("GET")
|
|
router.Handle("/debug/vars", expvar.Handler()).Methods("GET")
|
|
router.Handle("/metrics", promhttp.Handler())
|
|
|
|
router.HandleFunc("/metrics.json", handler.chkAuthZ(handler.handleGetMetricsJSON, authz.Admin)).Methods("GET").Name("GetMetricsJSON")
|
|
router.HandleFunc("/export", handler.chkAuthZ(handler.handleGetExport, authz.Read)).Methods("GET").Name("GetExport")
|
|
router.HandleFunc("/import-atomic-record", handler.chkAuthZ(handler.handlePostImportAtomicRecord, authz.Admin)).Methods("POST").Name("PostImportAtomicRecord")
|
|
router.HandleFunc("/index", handler.chkAuthZ(handler.handleGetIndexes, authz.Read)).Methods("GET").Name("GetIndexes")
|
|
router.HandleFunc("/index", handler.chkAuthZ(handler.handlePostIndex, authz.Admin)).Methods("POST").Name("PostIndex")
|
|
router.HandleFunc("/index/", handler.chkAuthZ(handler.handlePostIndex, authz.Admin)).Methods("POST").Name("PostIndex")
|
|
router.HandleFunc("/index/{index}", handler.chkAuthZ(handler.handleGetIndex, authz.Read)).Methods("GET").Name("GetIndex")
|
|
router.HandleFunc("/index/{index}", handler.chkAuthZ(handler.handlePostIndex, authz.Admin)).Methods("POST").Name("PostIndex")
|
|
router.HandleFunc("/index/{index}", handler.chkAuthZ(handler.handleDeleteIndex, authz.Admin)).Methods("DELETE").Name("DeleteIndex")
|
|
//router.HandleFunc("/index/{index}/field", handler.chkAuthZ(handler.handleGetFields, authz.Read)).Methods("GET") // Not implemented.
|
|
router.HandleFunc("/index/{index}/field", handler.chkAuthZ(handler.handlePostField, authz.Write)).Methods("POST").Name("PostField")
|
|
router.HandleFunc("/index/{index}/field/", handler.chkAuthZ(handler.handlePostField, authz.Write)).Methods("POST").Name("PostField")
|
|
router.HandleFunc("/index/{index}/field/{field}/view", handler.chkAuthZ(handler.handleGetView, authz.Admin)).Methods("GET")
|
|
router.HandleFunc("/index/{index}/field/{field}/view/{view}", handler.chkAuthZ(handler.handleDeleteView, authz.Admin)).Methods("DELETE").Name("DeleteView")
|
|
router.HandleFunc("/index/{index}/field/{field}", handler.chkAuthZ(handler.handlePostField, authz.Write)).Methods("POST").Name("PostField")
|
|
router.HandleFunc("/index/{index}/field/{field}", handler.chkAuthZ(handler.handlePatchField, authz.Write)).Methods("PATCH").Name("PatchField")
|
|
router.HandleFunc("/index/{index}/field/{field}", handler.chkAuthZ(handler.handleDeleteField, authz.Write)).Methods("DELETE").Name("DeleteField")
|
|
router.HandleFunc("/index/{index}/field/{field}/import", handler.chkAuthZ(handler.handlePostImport, authz.Write)).Methods("POST").Name("PostImport")
|
|
router.HandleFunc("/index/{index}/field/{field}/mutex-check", handler.chkAuthZ(handler.handleGetMutexCheck, authz.Read)).Methods("GET").Name("GetMutexCheck")
|
|
router.HandleFunc("/index/{index}/field/{field}/import-roaring/{shard}", handler.chkAuthZ(handler.handlePostImportRoaring, authz.Write)).Methods("POST").Name("PostImportRoaring")
|
|
router.HandleFunc("/index/{index}/shard/{shard}/import-roaring", handler.chkAuthZ(handler.handlePostShardImportRoaring, authz.Write)).Methods("POST").Name("PostImportRoaring")
|
|
router.HandleFunc("/index/{index}/query", handler.chkAuthZ(handler.handlePostQuery, authz.Read)).Methods("POST").Name("PostQuery")
|
|
router.HandleFunc("/info", handler.chkAuthZ(handler.handleGetInfo, authz.Admin)).Methods("GET").Name("GetInfo")
|
|
router.HandleFunc("/recalculate-caches", handler.chkAuthZ(handler.handleRecalculateCaches, authz.Admin)).Methods("POST").Name("RecalculateCaches")
|
|
router.HandleFunc("/schema", handler.chkAuthZ(handler.handleGetSchema, authz.Read)).Methods("GET").Name("GetSchema")
|
|
router.HandleFunc("/schema/details", handler.chkAuthZ(handler.handleGetSchemaDetails, authz.Read)).Methods("GET").Name("GetSchemaDetails")
|
|
router.HandleFunc("/schema", handler.chkAuthZ(handler.handlePostSchema, authz.Admin)).Methods("POST").Name("PostSchema")
|
|
router.HandleFunc("/status", handler.chkAuthZ(handler.handleGetStatus, authz.Read)).Methods("GET").Name("GetStatus")
|
|
router.HandleFunc("/transaction", handler.chkAuthZ(handler.handlePostTransaction, authz.Read)).Methods("POST").Name("PostTransaction")
|
|
router.HandleFunc("/transaction/", handler.chkAuthZ(handler.handlePostTransaction, authz.Read)).Methods("POST").Name("PostTransaction")
|
|
router.HandleFunc("/transaction/{id}", handler.chkAuthZ(handler.handleGetTransaction, authz.Read)).Methods("GET").Name("GetTransaction")
|
|
router.HandleFunc("/transaction/{id}", handler.chkAuthZ(handler.handlePostTransaction, authz.Read)).Methods("POST").Name("PostTransaction")
|
|
router.HandleFunc("/transaction/{id}/finish", handler.chkAuthZ(handler.handlePostFinishTransaction, authz.Read)).Methods("POST").Name("PostFinishTransaction")
|
|
router.HandleFunc("/transactions", handler.chkAuthZ(handler.handleGetTransactions, authz.Read)).Methods("GET").Name("GetTransactions")
|
|
router.HandleFunc("/queries", handler.chkAuthZ(handler.handleGetActiveQueries, authz.Admin)).Methods("GET").Name("GetActiveQueries")
|
|
|
|
// enable this endpoint based on config
|
|
if handler.sqlEnabled {
|
|
router.HandleFunc("/sql", handler.chkAuthZ(handler.handlePostSQL, authz.Admin)).Methods("POST").Name("PostSQL")
|
|
}
|
|
|
|
router.HandleFunc("/query-history", handler.chkAuthZ(handler.handleGetPastQueries, authz.Admin)).Methods("GET").Name("GetPastQueries")
|
|
router.HandleFunc("/version", handler.handleGetVersion).Methods("GET").Name("GetVersion")
|
|
|
|
// /ui endpoints are for UI use; they may change at any time.
|
|
router.HandleFunc("/ui/transaction", handler.chkAuthZ(handler.handleGetTransactionList, authz.Read)).Methods("GET").Name("GetTransactionList")
|
|
router.HandleFunc("/ui/transaction/", handler.chkAuthZ(handler.handleGetTransactionList, authz.Read)).Methods("GET").Name("GetTransactionList")
|
|
router.HandleFunc("/ui/shard-distribution", handler.chkAuthZ(handler.handleGetShardDistribution, authz.Admin)).Methods("GET").Name("GetShardDistribution")
|
|
|
|
// /internal endpoints are for internal use only; they may change at any time.
|
|
// DO NOT rely on these for external applications!
|
|
|
|
// Truly used internally by featurebase
|
|
router.HandleFunc("/internal/cluster/message", handler.chkInternal(handler.handlePostClusterMessage)).Methods("POST").Name("PostClusterMessage")
|
|
router.HandleFunc("/internal/translate/data", handler.chkAuthZ(handler.handleGetTranslateData, authz.Read)).Methods("GET").Name("GetTranslateData")
|
|
router.HandleFunc("/internal/translate/data", handler.chkAuthZ(handler.handlePostTranslateData, authz.Write)).Methods("POST").Name("PostTranslateData")
|
|
|
|
// other ones
|
|
router.HandleFunc("/internal/mem-usage", handler.chkAuthZ(handler.handleGetMemUsage, authz.Read)).Methods("GET").Name("GetUsage")
|
|
router.HandleFunc("/internal/disk-usage", handler.chkAuthZ(handler.handleGetDiskUsage, authz.Read)).Methods("GET").Name("GetUsage")
|
|
router.HandleFunc("/internal/disk-usage/{index}", handler.chkAuthZ(handler.handleGetDiskUsage, authz.Read)).Methods("GET").Name("GetUsage")
|
|
router.HandleFunc("/internal/fragment/block/data", handler.chkAuthN(handler.handleGetFragmentBlockData)).Methods("GET").Name("GetFragmentBlockData")
|
|
router.HandleFunc("/internal/fragment/blocks", handler.chkAuthN(handler.handleGetFragmentBlocks)).Methods("GET").Name("GetFragmentBlocks")
|
|
router.HandleFunc("/internal/fragment/data", handler.chkAuthN(handler.handleGetFragmentData)).Methods("GET").Name("GetFragmentData")
|
|
router.HandleFunc("/internal/fragment/nodes", handler.chkAuthN(handler.handleGetFragmentNodes)).Methods("GET").Name("GetFragmentNodes")
|
|
router.HandleFunc("/internal/partition/nodes", handler.chkAuthN(handler.handleGetPartitionNodes)).Methods("GET").Name("GetPartitionNodes")
|
|
router.HandleFunc("/internal/translate/keys", handler.chkAuthN(handler.handlePostTranslateKeys)).Methods("POST").Name("PostTranslateKeys")
|
|
router.HandleFunc("/internal/translate/ids", handler.chkAuthN(handler.handlePostTranslateIDs)).Methods("POST").Name("PostTranslateIDs")
|
|
router.HandleFunc("/internal/index/{index}/field/{field}/mutex-check", handler.chkAuthZ(handler.handleInternalGetMutexCheck, authz.Read)).Methods("GET").Name("InternalGetMutexCheck")
|
|
router.HandleFunc("/internal/index/{index}/field/{field}/remote-available-shards/{shardID}", handler.chkAuthZ(handler.handleDeleteRemoteAvailableShard, authz.Admin)).Methods("DELETE")
|
|
router.HandleFunc("/internal/index/{index}/shard/{shard}/snapshot", handler.chkAuthZ(handler.handleGetIndexShardSnapshot, authz.Read)).Methods("GET").Name("GetIndexShardSnapshot")
|
|
router.HandleFunc("/internal/index/{index}/shards", handler.chkAuthZ(handler.handleGetIndexAvailableShards, authz.Read)).Methods("GET").Name("GetIndexAvailableShards")
|
|
router.HandleFunc("/internal/nodes", handler.chkAuthN(handler.handleGetNodes)).Methods("GET").Name("GetNodes")
|
|
router.HandleFunc("/internal/shards/max", handler.chkAuthN(handler.handleGetShardsMax)).Methods("GET").Name("GetShardsMax") // TODO: deprecate, but it's being used by the client
|
|
|
|
router.HandleFunc("/internal/translate/index/{index}/keys/find", handler.chkAuthZ(handler.handleFindIndexKeys, authz.Admin)).Methods("POST").Name("FindIndexKeys")
|
|
router.HandleFunc("/internal/translate/index/{index}/keys/create", handler.chkAuthZ(handler.handleCreateIndexKeys, authz.Admin)).Methods("POST").Name("CreateIndexKeys")
|
|
router.HandleFunc("/internal/translate/index/{index}/{partition}", handler.chkAuthZ(handler.handlePostTranslateIndexDB, authz.Admin)).Methods("POST").Name("PostTranslateIndexDB")
|
|
router.HandleFunc("/internal/translate/field/{index}/{field}", handler.chkAuthZ(handler.handlePostTranslateFieldDB, authz.Admin)).Methods("POST").Name("PostTranslateFieldDB")
|
|
router.HandleFunc("/internal/translate/field/{index}/{field}/keys/find", handler.chkAuthZ(handler.handleFindFieldKeys, authz.Admin)).Methods("POST").Name("FindFieldKeys")
|
|
router.HandleFunc("/internal/translate/field/{index}/{field}/keys/create", handler.chkAuthZ(handler.handleCreateFieldKeys, authz.Admin)).Methods("POST").Name("CreateFieldKeys")
|
|
router.HandleFunc("/internal/translate/field/{index}/{field}/keys/like", handler.chkAuthZ(handler.handleMatchField, authz.Read)).Methods("POST").Name("MatchFieldKeys")
|
|
|
|
router.HandleFunc("/internal/idalloc/reserve", handler.chkAuthN(handler.handleReserveIDs)).Methods("POST").Name("ReserveIDs")
|
|
router.HandleFunc("/internal/idalloc/commit", handler.chkAuthN(handler.handleCommitIDs)).Methods("POST").Name("CommitIDs")
|
|
router.HandleFunc("/internal/idalloc/restore", handler.chkAuthN(handler.handleRestoreIDAlloc)).Methods("POST").Name("RestoreIDAllocData")
|
|
router.HandleFunc("/internal/idalloc/reset/{index}", handler.chkAuthN(handler.handleResetIDAlloc)).Methods("POST").Name("ResetIDAlloc")
|
|
router.HandleFunc("/internal/idalloc/data", handler.chkAuthN(handler.handleIDAllocData)).Methods("GET").Name("IDAllocData")
|
|
|
|
router.HandleFunc("/internal/restore/{index}/{shardID}", handler.chkAuthZ(handler.handlePostRestore, authz.Admin)).Methods("POST").Name("Restore")
|
|
|
|
router.HandleFunc("/internal/debug/rbf", handler.chkAuthZ(handler.handleGetInternalDebugRBFJSON, authz.Admin)).Methods("GET").Name("GetInternalDebugRBFJSON")
|
|
|
|
// endpoints for collecting cpu profiles from a chosen begin point to
|
|
// when the client wants to stop. Used for profiling imports that
|
|
// could be long or short.
|
|
router.HandleFunc("/cpu-profile/start", handler.chkAuthZ(handler.handleCPUProfileStart, authz.Admin)).Methods("GET").Name("CPUProfileStart")
|
|
router.HandleFunc("/cpu-profile/stop", handler.chkAuthZ(handler.handleCPUProfileStop, authz.Admin)).Methods("GET").Name("CPUProfileStop")
|
|
|
|
router.HandleFunc("/login", handler.handleLogin).Methods("GET").Name("Login")
|
|
router.HandleFunc("/logout", handler.handleLogout).Methods("GET").Name("Logout")
|
|
router.HandleFunc("/redirect", handler.handleRedirect).Methods("GET").Name("Redirect")
|
|
router.HandleFunc("/auth", handler.handleCheckAuthentication).Methods("GET").Name("CheckAuthentication")
|
|
router.HandleFunc("/userinfo", handler.handleUserInfo).Methods("GET").Name("UserInfo")
|
|
router.HandleFunc("/internal/oauth-config", handler.handleOAuthConfig).Methods("GET").Name("GetOAuthConfig")
|
|
|
|
router.HandleFunc("/health", handler.handleGetHealth).Methods("GET").Name("GetHealth")
|
|
router.HandleFunc("/directive", handler.handlePostDirective).Methods("POST").Name("PostDirective")
|
|
router.HandleFunc("/snapshot/shard-data", handler.handlePostSnapshotShardData).Methods("POST").Name("PostShapshotShardData")
|
|
router.HandleFunc("/snapshot/table-keys", handler.handlePostSnapshotTableKeys).Methods("POST").Name("PostShapshotTableKeys")
|
|
router.HandleFunc("/snapshot/field-keys", handler.handlePostSnapshotFieldKeys).Methods("POST").Name("PostShapshotFieldKeys")
|
|
|
|
// Endpoints to support lattice UI embedded via statik.
|
|
// The messiness here reflects the fact that assets live in a nontrivial
|
|
// directory structure that is controlled externally.
|
|
latticeHandler := newStatikHandler(handler)
|
|
router.PathPrefix("/static").Handler(latticeHandler)
|
|
router.Path("/").Handler(latticeHandler)
|
|
router.Path("/favicon.png").Handler(latticeHandler)
|
|
router.Path("/favicon.svg").Handler(latticeHandler)
|
|
router.Path("/manifest.json").Handler(latticeHandler)
|
|
for _, route := range latticeRoutes {
|
|
router.Path(route).Handler(latticeHandler)
|
|
}
|
|
|
|
router.Use(handler.queryArgValidator)
|
|
router.Use(handler.addQueryContext)
|
|
router.Use(handler.extractTracing)
|
|
router.Use(handler.monitorPerformance)
|
|
router.Use(handler.collectStats)
|
|
var h http.Handler = router
|
|
for _, middleware := range handler.middleware {
|
|
// Ideally, we would use `router.Use` to inject middleware,
|
|
// instead of wrapping the handler. The reason we can't is
|
|
// because the router will only apply middleware to matched
|
|
// handlers. In this case, it won't match handlers with the
|
|
// OPTIONS method, needed by the CORS middleware. This issue
|
|
// is described in detail here:
|
|
// https://github.com/gorilla/handlers/issues/142
|
|
h = middleware(h)
|
|
}
|
|
return h
|
|
}
|
|
|
|
// ServeHTTP handles an HTTP request.
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
stack := debug.Stack()
|
|
msg := "%s\n%s"
|
|
h.logger.Panicf(msg, err, stack)
|
|
fmt.Fprintf(w, msg, err, stack)
|
|
}
|
|
}()
|
|
|
|
h.Handler.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (h *Handler) chkInternal(handler http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if h.auth != nil {
|
|
secret, ok := r.Header["X-Feature-Key"]
|
|
var secretString string
|
|
if ok {
|
|
secretString = secret[0]
|
|
}
|
|
decodedString, err := hex.DecodeString(secretString)
|
|
if err != nil || !ok || !bytes.Equal(decodedString, h.auth.SecretKey()) {
|
|
http.Error(w, "internal secret key validation failed", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
handler.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) chkAllowedNetworks(r *http.Request) (bool, context.Context) {
|
|
// for every request, get IP of the request and check against configured IPs
|
|
reqIP := GetIP(r)
|
|
if reqIP == "" {
|
|
return false, r.Context()
|
|
}
|
|
|
|
// if client IP is in allowed networks
|
|
// add it to the context for key X-Molecula-Original-IP
|
|
if h.auth.CheckAllowedNetworks(reqIP) {
|
|
ctx := WithOriginalIP(r.Context(), reqIP)
|
|
return true, ctx
|
|
}
|
|
return false, r.Context()
|
|
}
|
|
|
|
func (h *Handler) chkAuthN(handler http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
//if the request is unauthenticated and we have the appropriate header get the userid from the header
|
|
requestUserID := r.Header.Get(HeaderRequestUserID)
|
|
ctx = WithUserID(ctx, requestUserID)
|
|
|
|
if h.auth == nil {
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// if IP is in allowed networks, then serve the request
|
|
allowedNetwork, ctx := h.chkAllowedNetworks(r)
|
|
if allowedNetwork {
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
access, refresh := getTokens(r)
|
|
uinfo, err := h.auth.Authenticate(access, refresh)
|
|
if err != nil {
|
|
http.Error(w, errors.Wrap(err, "authenticating").Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// prefer the user id from an authenticated request over one in a header
|
|
ctx = WithUserID(ctx, uinfo.UserID)
|
|
|
|
// just in case it got refreshed
|
|
ctx = authn.WithAccessToken(ctx, "Bearer"+access)
|
|
ctx = authn.WithRefreshToken(ctx, refresh)
|
|
h.auth.SetCookie(w, uinfo.Token, uinfo.RefreshToken, uinfo.Expiry)
|
|
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
|
|
func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
//if the request is unauthenticated and we have the appropriate header get the userid from the header
|
|
requestUserID := r.Header.Get(HeaderRequestUserID)
|
|
ctx = WithUserID(ctx, requestUserID)
|
|
|
|
// handle the case when auth is not turned on
|
|
if h.auth == nil {
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// check if IP is in allowed networks, if yes give it admin permissions
|
|
allowedNetwork, ctx := h.chkAllowedNetworks(r)
|
|
if allowedNetwork {
|
|
ctx = context.WithValue(ctx, contextKeyGroupMembership, []string{AllowedNetworksGroupName, h.permissions.Admin})
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// make a copy of the requested permissions
|
|
lperm := perm
|
|
|
|
// check if the user is authenticated
|
|
access, refresh := getTokens(r)
|
|
|
|
uinfo, err := h.auth.Authenticate(access, refresh)
|
|
if err != nil {
|
|
http.Error(w, errors.Wrap(err, "authenticating").Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// prefer the user id from an authenticated request over one in a header
|
|
ctx = WithUserID(ctx, uinfo.UserID)
|
|
|
|
ctx = authn.WithAccessToken(ctx, "Bearer "+access)
|
|
ctx = authn.WithRefreshToken(ctx, refresh)
|
|
|
|
// just in case it got refreshed
|
|
h.auth.SetCookie(w, uinfo.Token, uinfo.RefreshToken, uinfo.Expiry)
|
|
|
|
// put the user's authN/Z info in the context
|
|
ctx = context.WithValue(ctx, contextKeyGroupMembership, uinfo.Groups)
|
|
ctx = authn.WithAccessToken(ctx, "Bearer "+uinfo.Token)
|
|
ctx = authn.WithRefreshToken(ctx, uinfo.RefreshToken)
|
|
// unlikely h.permissions will be nil, but we'll check to be safe
|
|
if h.permissions == nil {
|
|
h.logger.Errorf("authentication is turned on without authorization permissions set")
|
|
http.Error(w, "authorizing", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// figure out what the user is querying for
|
|
queryString := ""
|
|
queryRequest := ctx.Value(contextKeyQueryRequest)
|
|
if req, ok := queryRequest.(*QueryRequest); ok {
|
|
queryString = req.Query
|
|
|
|
q, err := pql.ParseString(queryString)
|
|
if err != nil {
|
|
http.Error(w, errors.Wrap(err, "parsing query string").Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// if there are write calls, and the needed perms don't already
|
|
// satisfy write permissions, then make them write permissions
|
|
if q.WriteCallN() > 0 && !lperm.Satisfies(authz.Write) {
|
|
lperm = authz.Write
|
|
}
|
|
}
|
|
// make the query string pretty
|
|
queryString = strings.Replace(queryString, "\n", "", -1)
|
|
|
|
// figure out if we should log this query
|
|
toLog := true
|
|
for _, ep := range []string{"/status", "/metrics", "/info", "/internal"} {
|
|
if strings.HasPrefix(r.URL.Path, ep) {
|
|
toLog = false
|
|
break
|
|
}
|
|
}
|
|
if toLog {
|
|
h.queryLogger.Infof("%v, %v, %v, %v, %v, %v", GetIP(r), r.UserAgent(), r.URL.Path, uinfo.UserID, uinfo.UserName, queryString)
|
|
}
|
|
|
|
// if they're an admin, they can do whatever they want
|
|
if h.permissions.IsAdmin(uinfo.Groups) {
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
} else if lperm == authz.Admin {
|
|
// if they're not an admin, and they need to be, we can just
|
|
// error right here
|
|
http.Error(w, "Insufficient permissions: user does not have admin permission", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// try to get the index name
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
indexName = r.URL.Query().Get("index")
|
|
}
|
|
|
|
// if we have an index name, then we check the user permissions
|
|
// against that index
|
|
if indexName != "" {
|
|
p, err := h.permissions.GetPermissions(uinfo, indexName)
|
|
if err != nil {
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
http.Error(w, errors.Wrap(err, "Insufficient Permissions").Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// if they're not permitted to access this index, error
|
|
if !p.Satisfies(lperm) {
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
http.Error(w, "Insufficient permissions", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
|
|
func GetIP(r *http.Request) string {
|
|
// check if original IP was set in the request
|
|
og := r.Header.Get(OriginalIPHeader)
|
|
if og != "" {
|
|
return og
|
|
}
|
|
|
|
// check if original IP is in the context
|
|
if ogIP, ok := OriginalIPFromContext(r.Context()); ok && ogIP != "" {
|
|
return ogIP
|
|
}
|
|
|
|
// X-Forwarded-For can have multiple IPs
|
|
// the first IP will always be the originating client IP
|
|
// the remaining IPs will be for any proxies the request went through
|
|
forwarded := r.Header.Get(ForwardedIPHeader)
|
|
forwardedList := strings.Split(forwarded, ",")
|
|
if forwardedList[0] != "" {
|
|
return forwardedList[0]
|
|
|
|
}
|
|
|
|
return r.RemoteAddr
|
|
}
|
|
|
|
// statikHandler implements the http.Handler interface, and responds to
|
|
// requests for static assets with the appropriate file contents embedded
|
|
// in a statik filesystem.
|
|
type statikHandler struct {
|
|
handler *Handler
|
|
statikFS http.FileSystem
|
|
}
|
|
|
|
// newStatikHandler returns a new instance of statikHandler
|
|
func newStatikHandler(h *Handler) statikHandler {
|
|
fs, err := h.fileSystem.New()
|
|
if err == nil {
|
|
h.logger.Printf("enabled Web UI at %s", h.url)
|
|
}
|
|
|
|
return statikHandler{
|
|
handler: h,
|
|
statikFS: fs,
|
|
}
|
|
}
|
|
|
|
func (s statikHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.UserAgent(), "curl") {
|
|
msg := "Welcome. FeatureBase v" + s.handler.api.Version() + " is running. Visit https://docs.featurebase.com for more information."
|
|
if s.statikFS != nil {
|
|
msg += " Try the Web UI by visiting this URL in your browser."
|
|
}
|
|
http.Error(w, msg, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if s.statikFS == nil {
|
|
msg := "Web UI is not available. Please run `make generate-statik` before building Pilosa with `make install`."
|
|
s.handler.logger.Infof(msg)
|
|
http.Error(w, msg, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Without this check, refreshing the UI at e.g. /query
|
|
// will request a nonexistent resource and return 404.
|
|
for _, route := range latticeRoutes {
|
|
if r.URL.String() == route {
|
|
url, _ := url.Parse("/")
|
|
r.URL = url
|
|
}
|
|
}
|
|
|
|
http.FileServer(s.statikFS).ServeHTTP(w, r)
|
|
}
|
|
|
|
// successResponse is a general success/error struct for http responses.
|
|
type successResponse struct {
|
|
h *Handler
|
|
Success bool `json:"success"`
|
|
Name string `json:"name,omitempty"`
|
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
|
Error *HTTPError `json:"error,omitempty"`
|
|
}
|
|
|
|
// Error defines a standard application error.
|
|
type HTTPError struct {
|
|
// Human-readable message.
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// Error returns the string representation of the error message.
|
|
func (e *HTTPError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// check determines success or failure based on the error.
|
|
// It also returns the corresponding http status code.
|
|
func (r *successResponse) check(err error) (statusCode int) {
|
|
if err == nil {
|
|
r.Success = true
|
|
return 0
|
|
}
|
|
|
|
cause := errors.Cause(err)
|
|
|
|
// Determine HTTP status code based on the error type.
|
|
switch cause.(type) {
|
|
case BadRequestError:
|
|
statusCode = http.StatusBadRequest
|
|
case ConflictError:
|
|
statusCode = http.StatusConflict
|
|
case NotFoundError:
|
|
// TODO I think any error matches NotFoundError because it's a `type NotFoundError error`
|
|
statusCode = http.StatusNotFound
|
|
default:
|
|
statusCode = http.StatusInternalServerError
|
|
}
|
|
|
|
r.Success = false
|
|
r.Error = &HTTPError{Message: err.Error()}
|
|
|
|
return statusCode
|
|
}
|
|
|
|
// write sends a response to the http.ResponseWriter based on the success
|
|
// status and the error.
|
|
func (r *successResponse) write(w http.ResponseWriter, err error) {
|
|
// Apply the error and get the status code.
|
|
statusCode := r.check(err)
|
|
|
|
// Marshal the json response.
|
|
msg, err := json.Marshal(r)
|
|
if err != nil {
|
|
http.Error(w, string(msg), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Write the response.
|
|
if statusCode == 0 {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, err := w.Write(msg)
|
|
if err != nil {
|
|
r.h.logger.Errorf("error writing response: %v", err)
|
|
return
|
|
}
|
|
_, err = w.Write([]byte("\n"))
|
|
if err != nil {
|
|
r.h.logger.Errorf("error writing newline after response: %v", err)
|
|
return
|
|
}
|
|
} else {
|
|
http.Error(w, string(msg), statusCode)
|
|
}
|
|
}
|
|
|
|
// validHeaderAcceptJSON returns false if one or more Accept
|
|
// headers are present, but none of them are "application/json"
|
|
// (or any matching wildcard). Otherwise returns true.
|
|
func validHeaderAcceptJSON(header http.Header) bool {
|
|
return validHeaderAcceptType(header, "application", "json")
|
|
}
|
|
|
|
func validHeaderAcceptType(header http.Header, typ, subtyp string) bool {
|
|
if v, found := header["Accept"]; found {
|
|
for _, v := range v {
|
|
t, _, err := mime.ParseMediaType(v)
|
|
if err != nil {
|
|
switch err {
|
|
case mime.ErrInvalidMediaParameter:
|
|
// This is an optional feature, so we can keep going anyway.
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
spl := strings.SplitN(t, "/", 2)
|
|
if len(spl) < 2 {
|
|
continue
|
|
}
|
|
switch {
|
|
case spl[0] == typ && spl[1] == subtyp:
|
|
return true
|
|
case spl[0] == "*" && spl[1] == subtyp:
|
|
return true
|
|
case spl[0] == typ && spl[1] == "*":
|
|
return true
|
|
case spl[0] == "*" && spl[1] == "*":
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// headerAcceptRoaringRow tells us that the request should accept roaring
|
|
// rows in response.
|
|
func headerAcceptRoaringRow(header http.Header) bool {
|
|
for _, v := range header["X-Pilosa-Row"] {
|
|
if v == "roaring" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *Handler) filterSchema(schema []*IndexInfo, g []authn.Group) []*IndexInfo {
|
|
if !h.permissions.IsAdmin(g) {
|
|
var filtered []*IndexInfo
|
|
allowed := h.permissions.GetAuthorizedIndexList(g, authz.Read)
|
|
for _, s := range schema {
|
|
for _, index := range allowed {
|
|
if s.Name == index {
|
|
filtered = append(filtered, s)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
schema = filtered
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func (h *Handler) getGroupMembership(r *http.Request) (g []authn.Group) {
|
|
// if IP is in allowed networks, then give admin group membership
|
|
if h.auth.CheckAllowedNetworks(GetIP(r)) {
|
|
g = []authn.Group{
|
|
{
|
|
GroupID: h.permissions.Admin,
|
|
GroupName: AllowedNetworksGroupName,
|
|
},
|
|
}
|
|
return g
|
|
}
|
|
|
|
// check if group membership was already set in request when auth token was obtained
|
|
g = r.Context().Value(contextKeyGroupMembership).([]authn.Group)
|
|
return g
|
|
}
|
|
|
|
// handleGetSchema handles GET /schema requests.
|
|
func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
q := r.URL.Query()
|
|
withViews := q.Get("views") == "true"
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
schema, err := h.api.Schema(r.Context(), withViews)
|
|
if err != nil {
|
|
h.logger.Printf("getting schema error: %s", err)
|
|
}
|
|
|
|
// if auth is turned on, filter response to only include authorized indexes
|
|
if h.auth != nil {
|
|
g := h.getGroupMembership(r)
|
|
if g == nil {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
schema = h.filterSchema(schema, g)
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(Schema{Indexes: schema}); err != nil {
|
|
h.logger.Errorf("write schema response error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetSchema handles GET /schema/details requests. This is essentially the
|
|
// same thing as a GET /schema request, except WithViews is turned on by default.
|
|
// Previously, /schema/details returned the cardinality of each field, but this was
|
|
// removed for performance reasons. If, at some point in the future, there is a more
|
|
// performant way to get the cardinality of a field, that information would be
|
|
// included here.
|
|
func (h *Handler) handleGetSchemaDetails(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
schema, err := h.api.Schema(r.Context(), true)
|
|
if err != nil {
|
|
h.logger.Printf("error getting detailed schema: %s", err)
|
|
return
|
|
}
|
|
|
|
// if auth is turned on, filter response to only include authorized indexes
|
|
if h.auth != nil {
|
|
g := h.getGroupMembership(r)
|
|
if g == nil {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
schema = h.filterSchema(schema, g)
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(Schema{Indexes: schema}); err != nil {
|
|
h.logger.Printf("write schema response error: %s", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handlePostSchema(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
remoteStr := q.Get("remote")
|
|
var remote bool
|
|
if remoteStr == "true" {
|
|
remote = true
|
|
}
|
|
|
|
schema := &Schema{}
|
|
if err := json.NewDecoder(r.Body).Decode(schema); err != nil {
|
|
http.Error(w, fmt.Sprintf("decoding request as JSON Pilosa schema: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.api.ApplySchema(r.Context(), schema, remote); err != nil {
|
|
http.Error(w, fmt.Sprintf("apply schema to Pilosa: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// handleGetMemUsage handles GET /internal/mem-usage requests.
|
|
func (h *Handler) handleGetMemUsage(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
use, err := GetMemoryUsage()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(use); err != nil {
|
|
h.logger.Errorf("write mem usage response error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetDiskUsage handles GET /internal/disk-usage requests.
|
|
func (h *Handler) handleGetDiskUsage(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
u := h.api.server.dataDir
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if ok {
|
|
u = fmt.Sprintf("%s/indexes/%s", u, indexName)
|
|
}
|
|
|
|
use, err := GetDiskUsage(u)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(use); err != nil {
|
|
h.logger.Errorf("write disk usage response error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetShardDistribution handles GET /ui/shard-distribution requests.
|
|
func (h *Handler) handleGetShardDistribution(w http.ResponseWriter, r *http.Request) {
|
|
dist := h.api.ShardDistribution(r.Context())
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(dist); err != nil {
|
|
h.logger.Errorf("write status response error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetStatus handles GET /status requests.
|
|
func (h *Handler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
state, err := h.api.State()
|
|
if err != nil {
|
|
http.Error(w, "getting cluster state error: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
status := getStatusResponse{
|
|
State: string(state),
|
|
Nodes: h.api.Hosts(r.Context()),
|
|
LocalID: h.api.Node().ID,
|
|
ClusterName: h.api.ClusterName(),
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(status); err != nil {
|
|
h.logger.Errorf("write status response error: %s", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleGetInfo(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
info := h.api.Info()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(info); err != nil {
|
|
h.logger.Errorf("write info response error: %s", err)
|
|
}
|
|
}
|
|
|
|
type getSchemaResponse struct {
|
|
Indexes []*IndexInfo `json:"indexes"`
|
|
}
|
|
|
|
type getStatusResponse struct {
|
|
State string `json:"state"`
|
|
Nodes []*disco.Node `json:"nodes"`
|
|
LocalID string `json:"localID"`
|
|
ClusterName string `json:"clusterName"`
|
|
}
|
|
|
|
func httpHash(s string) string {
|
|
hasher := blake3.New()
|
|
_, _ = hasher.Write([]byte(s))
|
|
var buf [16]byte
|
|
_, _ = hasher.Digest().Read(buf[0:])
|
|
|
|
return fmt.Sprintf("%x", buf)
|
|
}
|
|
|
|
var DoPerQueryProfiling = false
|
|
|
|
// handlePostQuery handles /query requests.
|
|
func (h *Handler) handlePostQuery(w http.ResponseWriter, r *http.Request) {
|
|
// Read previouly parsed request from context
|
|
qreq := r.Context().Value(contextKeyQueryRequest)
|
|
qerr := r.Context().Value(contextKeyQueryError)
|
|
req, ok := qreq.(*QueryRequest)
|
|
|
|
if DoPerQueryProfiling {
|
|
backend := storage.DefaultBackend
|
|
reqHash := httpHash(req.Query)
|
|
|
|
qlen := len(req.Query)
|
|
if qlen > 100 {
|
|
qlen = 100
|
|
}
|
|
name := "_query." + reqHash + "." + backend + "." + time.Now().Format("20060102150405") + "." + req.Query[:qlen]
|
|
f, err := os.Create(name)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
_ = pprof.StartCPUProfile(f)
|
|
defer pprof.StopCPUProfile()
|
|
|
|
} // end DoPerQueryProfiling
|
|
|
|
var err error
|
|
err, _ = qerr.(error)
|
|
if err != nil || !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
e := h.writeQueryResponse(w, r, &QueryResponse{Err: err})
|
|
if e != nil {
|
|
h.logger.Errorf("write query response error: %v (while trying to write another error: %v)", e, err)
|
|
}
|
|
return
|
|
}
|
|
// TODO: Remove
|
|
req.Index = mux.Vars(r)["index"]
|
|
|
|
resp, err := h.api.Query(r.Context(), req)
|
|
if err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrTooManyWrites:
|
|
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
|
case ErrTranslateStoreReadOnly:
|
|
u := h.api.PrimaryReplicaNodeURL()
|
|
u.Path, u.RawQuery = r.URL.Path, r.URL.RawQuery
|
|
http.Redirect(w, r, u.String(), http.StatusFound)
|
|
return
|
|
default:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
e := h.writeQueryResponse(w, r, &QueryResponse{Err: err})
|
|
if e != nil {
|
|
h.logger.Errorf("write query response error: %v (while trying to write another error: %v)", e, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Set appropriate status code, if there is an error. It doesn't appear that
|
|
// resp.Err could ever be set in API.Query, so this code block is probably
|
|
// doing nothing right now.
|
|
if resp.Err != nil {
|
|
switch errors.Cause(resp.Err) {
|
|
case ErrTooManyWrites:
|
|
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
|
default:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
// Write response back to client.
|
|
if err := h.writeQueryResponse(w, r, &resp); err != nil {
|
|
h.logger.Errorf("write query response error: %s", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) writeBadRequest(w http.ResponseWriter, r *http.Request, err error) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
e := h.writeQueryResponse(w, r, &QueryResponse{Err: err})
|
|
if e != nil {
|
|
h.logger.Errorf("write query response error: %v (while trying to write another error: %v)", e, err)
|
|
}
|
|
|
|
}
|
|
|
|
// handlePostSQL handles /sql requests.
|
|
func (h *Handler) handlePostSQL(w http.ResponseWriter, r *http.Request) {
|
|
|
|
b, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
h.writeBadRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
rootOperator, err := h.api.CompilePlan(r.Context(), string(b))
|
|
if err != nil {
|
|
h.writeBadRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// Write response back to client.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// the pandas data frame format in json
|
|
|
|
// Opening bracket.
|
|
w.Write([]byte("{"))
|
|
|
|
// Write the closing bracket on any exit from this method.
|
|
defer func() {
|
|
duration := time.Since(start)
|
|
value, err := json.Marshal(duration.Microseconds())
|
|
if err != nil {
|
|
value = big.NewInt(-1).Bytes()
|
|
}
|
|
w.Write([]byte(`,"exec_time":`))
|
|
w.Write(value)
|
|
w.Write([]byte("}"))
|
|
}()
|
|
|
|
// writeError is a helper function that can be called anywhere during the
|
|
// output handling to insert an error into the json output.
|
|
writeError := func(err error) {
|
|
if err != nil {
|
|
errMsg, err := json.Marshal(err.Error())
|
|
if err != nil {
|
|
errMsg = []byte(`"PROBLEM ENCODING ERROR MESSAGE"`)
|
|
}
|
|
w.Write([]byte(`,"error":`))
|
|
w.Write(errMsg)
|
|
}
|
|
}
|
|
|
|
// writeWarnings is a helper function that can be called anywhere during the
|
|
// output handling to insert warnings into the json output.
|
|
writeWarnings := func(warnings []string) {
|
|
if len(warnings) > 0 {
|
|
w.Write([]byte(`,"warnings": [`))
|
|
for i, warn := range warnings {
|
|
warnMsg, err := json.Marshal(warn)
|
|
if err != nil {
|
|
warnMsg = []byte(`"PROBLEM ENCODING WARNING"`)
|
|
}
|
|
w.Write(warnMsg)
|
|
if i < len(warnings)-1 {
|
|
w.Write([]byte(`,`))
|
|
}
|
|
}
|
|
w.Write([]byte(`]`))
|
|
}
|
|
}
|
|
|
|
// Get a query iterator.
|
|
iter, err := rootOperator.Iterator(r.Context(), nil)
|
|
if err != nil {
|
|
writeError(err)
|
|
writeWarnings(rootOperator.Warnings())
|
|
return
|
|
}
|
|
|
|
// Read schema & write to response.
|
|
columns := rootOperator.Schema()
|
|
schema := SQLSchema{
|
|
Fields: make([]*SQLField, len(columns)),
|
|
}
|
|
for i, col := range columns {
|
|
schema.Fields[i] = &SQLField{
|
|
Name: col.ColumnName,
|
|
Type: col.Type.TypeName(),
|
|
}
|
|
}
|
|
w.Write([]byte(`"schema":`))
|
|
jsonSchema, err := json.Marshal(schema)
|
|
if err != nil {
|
|
h.logger.Errorf("write schema response error: %s", err)
|
|
// Provide an empty list as the schema value to maintain valid json.
|
|
w.Write([]byte("[]"))
|
|
writeError(err)
|
|
writeWarnings(rootOperator.Warnings())
|
|
return
|
|
}
|
|
w.Write(jsonSchema)
|
|
|
|
// Write the data (rows).
|
|
w.Write([]byte(`,"data":[`))
|
|
|
|
var rowErr error
|
|
var currentRow types.Row
|
|
var nextErr error
|
|
|
|
rowCounter := 1
|
|
for currentRow, nextErr = iter.Next(r.Context()); nextErr == nil; currentRow, nextErr = iter.Next(r.Context()) {
|
|
jsonRow, err := json.Marshal(currentRow)
|
|
if err != nil {
|
|
h.logger.Errorf("json encoding error: %s", err)
|
|
rowErr = err
|
|
break
|
|
}
|
|
|
|
if rowCounter > 1 {
|
|
// Include a comma between data rows.
|
|
w.Write([]byte(","))
|
|
}
|
|
w.Write(jsonRow)
|
|
|
|
rowCounter++
|
|
}
|
|
if nextErr != nil && nextErr != types.ErrNoMoreRows {
|
|
rowErr = nextErr
|
|
}
|
|
|
|
w.Write([]byte("]"))
|
|
|
|
writeError(rowErr)
|
|
writeWarnings(rootOperator.Warnings())
|
|
}
|
|
|
|
type SQLField struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type SQLSchema struct {
|
|
Fields []*SQLField `json:"fields"`
|
|
}
|
|
|
|
func (h *Handler) handleCPUProfileStart(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if h.pprofCPUProfileBuffer == nil {
|
|
h.pprofCPUProfileBuffer = bytes.NewBuffer(nil)
|
|
} else {
|
|
http.Error(w, "cpu profile already in progress", http.StatusBadRequest)
|
|
return
|
|
}
|
|
err := pprof.StartCPUProfile(h.pprofCPUProfileBuffer)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
|
|
h.pprofCPUProfileBuffer = nil
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *Handler) handleCPUProfileStop(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if h.pprofCPUProfileBuffer == nil {
|
|
http.Error(w, "no cpu profile in progress", http.StatusBadRequest)
|
|
return
|
|
}
|
|
pprof.StopCPUProfile()
|
|
|
|
// match what pprof usually returns:
|
|
// HTTP/1.1 200 OK
|
|
// Content-Disposition: attachment; filename="profile"
|
|
// Content-Type: application/octet-stream
|
|
// X-Content-Type-Options: nosniff
|
|
// Date: Tue, 03 Nov 2020 18:31:36 GMT
|
|
// Content-Length: 939
|
|
|
|
//Send the headers
|
|
by := h.pprofCPUProfileBuffer.Bytes()
|
|
w.Header().Set("Content-Disposition", "attachment; filename=\"profile\"")
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%v", len(by)))
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
_, err := io.Copy(w, h.pprofCPUProfileBuffer)
|
|
h.pprofCPUProfileBuffer = nil
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// handleGetIndexAvailableShards handles GET /internal/index/:index/shards requests.
|
|
func (h *Handler) handleGetIndexAvailableShards(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
shards, err := h.api.AvailableShards(r.Context(), indexName)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(getIndexAvailableShardsResponse{Shards: shards.Slice()}); err != nil {
|
|
h.logger.Errorf("write shards-max response error: %s", err)
|
|
}
|
|
}
|
|
|
|
type getIndexAvailableShardsResponse struct {
|
|
Shards []uint64 `json:"shards"`
|
|
}
|
|
|
|
// handleGetShardsMax handles GET /internal/shards/max requests.
|
|
func (h *Handler) handleGetShardsMax(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(getShardsMaxResponse{
|
|
Standard: h.api.MaxShards(r.Context()),
|
|
}); err != nil {
|
|
h.logger.Errorf("write shards-max response error: %s", err)
|
|
}
|
|
}
|
|
|
|
type getShardsMaxResponse struct {
|
|
Standard map[string]uint64 `json:"standard"`
|
|
}
|
|
|
|
// handleGetIndexes handles GET /index request.
|
|
func (h *Handler) handleGetIndexes(w http.ResponseWriter, r *http.Request) {
|
|
h.handleGetSchema(w, r)
|
|
}
|
|
|
|
// handleGetIndex handles GET /index/<indexname> requests.
|
|
func (h *Handler) handleGetIndex(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
q := r.URL.Query()
|
|
withViews := q.Get("views") == "true"
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
schema, err := h.api.Schema(r.Context(), withViews)
|
|
if err != nil {
|
|
h.logger.Printf("getting schema error: %s", err)
|
|
}
|
|
|
|
for _, idx := range schema {
|
|
if idx.Name == indexName {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(idx); err != nil {
|
|
h.logger.Errorf("write response error: %s", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
http.Error(w, fmt.Sprintf("Index %s Not Found", indexName), http.StatusNotFound)
|
|
}
|
|
|
|
// handleGetView handles GET /index/<indexname>/field/<fieldname>/view requests.
|
|
func (h *Handler) handleGetView(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
fieldName := mux.Vars(r)["field"]
|
|
index, err := h.api.Index(r.Context(), indexName)
|
|
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Index %s Not Found", indexName), http.StatusNotFound)
|
|
return
|
|
} else {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var viewsList []viewReponse
|
|
for _, field := range index.fields {
|
|
if field.name == fieldName {
|
|
for _, view := range field.views() {
|
|
viewsList = append(viewsList, viewReponse{Name: view.name, Type: view.fieldType, Field: view.field, Index: view.index})
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(viewsList); err != nil {
|
|
h.logger.Errorf("write response error: %s", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
http.Error(w, fmt.Sprintf("Field %s Not Found", fieldName), http.StatusNotFound)
|
|
}
|
|
|
|
type viewReponse struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Field string `json:"field"`
|
|
Index string `json:"index"`
|
|
}
|
|
|
|
// handleDeleteIndex handles DELETE /index/<indexname>/field/<fieldname>/view/<viewname> request.
|
|
func (h *Handler) handleDeleteView(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
fieldName := mux.Vars(r)["field"]
|
|
viewName := mux.Vars(r)["view"]
|
|
|
|
resp := successResponse{h: h}
|
|
err := h.api.DeleteView(r.Context(), indexName, fieldName, viewName)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
type postIndexRequest struct {
|
|
Options IndexOptions `json:"options"`
|
|
}
|
|
|
|
// _postIndexRequest is necessary to avoid recursion while decoding.
|
|
type _postIndexRequest postIndexRequest
|
|
|
|
// Custom Unmarshal JSON to validate request body when creating a new index.
|
|
func (p *postIndexRequest) UnmarshalJSON(b []byte) error {
|
|
|
|
// m is an overflow map used to capture additional, unexpected keys.
|
|
m := make(map[string]interface{})
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
return errors.Wrap(err, "unmarshalling unexpected values")
|
|
}
|
|
|
|
validIndexOptions := getValidOptions(IndexOptions{})
|
|
err := validateOptions(m, validIndexOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Unmarshal expected values.
|
|
_p := _postIndexRequest{
|
|
Options: IndexOptions{
|
|
Description: "",
|
|
Keys: false,
|
|
TrackExistence: true,
|
|
},
|
|
}
|
|
if err := json.Unmarshal(b, &_p); err != nil {
|
|
return errors.Wrap(err, "unmarshalling expected values")
|
|
}
|
|
|
|
p.Options = _p.Options
|
|
|
|
return nil
|
|
}
|
|
|
|
func getValidOptions(option interface{}) []string {
|
|
validOptions := []string{}
|
|
val := reflect.ValueOf(option)
|
|
for i := 0; i < val.Type().NumField(); i++ {
|
|
jsonTag := val.Type().Field(i).Tag.Get("json")
|
|
s := strings.Split(jsonTag, ",")
|
|
validOptions = append(validOptions, s[0])
|
|
}
|
|
return validOptions
|
|
}
|
|
|
|
// Raise errors for any unknown key
|
|
func validateOptions(data map[string]interface{}, validIndexOptions []string) error {
|
|
for k, v := range data {
|
|
switch k {
|
|
case "options":
|
|
options, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
return errors.New("options is not map[string]interface{}")
|
|
}
|
|
for kk, vv := range options {
|
|
if !foundItem(validIndexOptions, kk) {
|
|
return fmt.Errorf("unknown key: %v:%v", kk, vv)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown key: %v:%v", k, v)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func foundItem(items []string, item string) bool {
|
|
for _, i := range items {
|
|
if item == i {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// handleDeleteIndex handles DELETE /index request.
|
|
func (h *Handler) handleDeleteIndex(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
|
|
resp := successResponse{h: h}
|
|
err := h.api.DeleteIndex(r.Context(), indexName)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
// handlePostIndex handles POST /index request.
|
|
func (h *Handler) handlePostIndex(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := successResponse{h: h, Name: indexName}
|
|
|
|
// Decode request.
|
|
req := postIndexRequest{
|
|
Options: IndexOptions{
|
|
Description: "",
|
|
Keys: false,
|
|
TrackExistence: true,
|
|
},
|
|
}
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil && err != io.EOF {
|
|
resp.write(w, err)
|
|
return
|
|
}
|
|
index, err := h.api.CreateIndex(r.Context(), indexName, req.Options)
|
|
|
|
if index != nil {
|
|
resp.CreatedAt = index.CreatedAt()
|
|
} else if _, ok = errors.Cause(err).(ConflictError); ok {
|
|
if index, _ = h.api.Index(r.Context(), indexName); index != nil {
|
|
resp.CreatedAt = index.CreatedAt()
|
|
}
|
|
}
|
|
resp.write(w, err)
|
|
}
|
|
|
|
func (h *Handler) handleGetActiveQueries(w http.ResponseWriter, r *http.Request) {
|
|
var rtype string
|
|
switch {
|
|
case validHeaderAcceptType(r.Header, "text", "plain"):
|
|
rtype = "text/plain"
|
|
case validHeaderAcceptJSON(r.Header):
|
|
rtype = "application/json"
|
|
default:
|
|
http.Error(w, "no acceptable response type selected", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
queries, err := h.api.ActiveQueries(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", rtype)
|
|
switch rtype {
|
|
case "text/plain":
|
|
durations := make([]string, len(queries))
|
|
for i, q := range queries {
|
|
durations[i] = q.Age.String()
|
|
}
|
|
var maxlen int
|
|
for _, l := range durations {
|
|
if len(l) > maxlen {
|
|
maxlen = len(l)
|
|
}
|
|
}
|
|
for i, q := range queries {
|
|
_, err := fmt.Fprintf(w, "%*s%q\n", -(maxlen + 2), durations[i], q.PQL)
|
|
if err != nil {
|
|
h.logger.Errorf("sending GetActiveQueries response: %s", err)
|
|
return
|
|
}
|
|
}
|
|
if _, err := w.Write([]byte{'\n'}); err != nil {
|
|
h.logger.Errorf("sending GetActiveQueries response: %s", err)
|
|
}
|
|
case "application/json":
|
|
if err := json.NewEncoder(w).Encode(queries); err != nil {
|
|
h.logger.Errorf("encoding GetActiveQueries response: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleGetPastQueries(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
remoteStr := q.Get("remote")
|
|
var remote bool
|
|
if remoteStr == "true" {
|
|
remote = true
|
|
}
|
|
|
|
queries, err := h.api.PastQueries(r.Context(), remote)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(queries); err != nil {
|
|
h.logger.Errorf("encoding GetActiveQueries response: %s", err)
|
|
}
|
|
|
|
}
|
|
|
|
func fieldOptionsToFunctionalOpts(opt fieldOptions) []FieldOption {
|
|
// Convert json options into functional options.
|
|
var fos []FieldOption
|
|
switch opt.Type {
|
|
case FieldTypeSet:
|
|
fos = append(fos, OptFieldTypeSet(*opt.CacheType, *opt.CacheSize))
|
|
case FieldTypeInt:
|
|
min, max := pql.MinMax(0)
|
|
// ensure the provided bounds are valid
|
|
if opt.Max != nil && max.LessThan(*opt.Max) {
|
|
opt.Max = &max
|
|
}
|
|
if opt.Max == nil {
|
|
opt.Max = &max
|
|
}
|
|
|
|
if opt.Min != nil && min.GreaterThan(*opt.Min) {
|
|
opt.Min = &min
|
|
}
|
|
if opt.Min == nil {
|
|
opt.Min = &min
|
|
}
|
|
fos = append(fos, OptFieldTypeInt(opt.Min.ToInt64(0), opt.Max.ToInt64(0)))
|
|
case FieldTypeDecimal:
|
|
scale := int64(0)
|
|
if opt.Scale != nil {
|
|
scale = *opt.Scale
|
|
min, max := pql.MinMax(scale)
|
|
// ensure the provided bounds are valid
|
|
if opt.Max != nil && max.LessThan(*opt.Max) {
|
|
opt.Max = &max
|
|
}
|
|
if opt.Max == nil {
|
|
opt.Max = &max
|
|
}
|
|
|
|
if opt.Min != nil && min.GreaterThan(*opt.Min) {
|
|
opt.Min = &min
|
|
}
|
|
if opt.Min == nil {
|
|
opt.Min = &min
|
|
}
|
|
}
|
|
|
|
if opt.Min == nil {
|
|
min := pql.NewDecimal(int64(math.MinInt64), scale)
|
|
opt.Min = &min
|
|
}
|
|
if opt.Max == nil {
|
|
max := pql.NewDecimal(int64(math.MaxInt64), scale)
|
|
opt.Max = &max
|
|
}
|
|
var minmax []pql.Decimal
|
|
if opt.Min != nil {
|
|
minmax = []pql.Decimal{
|
|
*opt.Min,
|
|
}
|
|
if opt.Max != nil {
|
|
minmax = append(minmax, *opt.Max)
|
|
}
|
|
}
|
|
fos = append(fos, OptFieldTypeDecimal(scale, minmax...))
|
|
case FieldTypeTimestamp:
|
|
if opt.Epoch == nil {
|
|
epoch := DefaultEpoch
|
|
opt.Epoch = &epoch
|
|
}
|
|
fos = append(fos, OptFieldTypeTimestamp(opt.Epoch.UTC(), *opt.TimeUnit))
|
|
case FieldTypeTime:
|
|
if opt.TTL != nil {
|
|
fos = append(fos, OptFieldTypeTime(*opt.TimeQuantum, *opt.TTL, opt.NoStandardView))
|
|
} else {
|
|
fos = append(fos, OptFieldTypeTime(*opt.TimeQuantum, "0", opt.NoStandardView))
|
|
}
|
|
case FieldTypeMutex:
|
|
fos = append(fos, OptFieldTypeMutex(*opt.CacheType, *opt.CacheSize))
|
|
case FieldTypeBool:
|
|
fos = append(fos, OptFieldTypeBool())
|
|
}
|
|
if opt.Keys != nil {
|
|
if *opt.Keys {
|
|
fos = append(fos, OptFieldKeys())
|
|
}
|
|
}
|
|
if opt.ForeignIndex != nil {
|
|
fos = append(fos, OptFieldForeignIndex(*opt.ForeignIndex))
|
|
}
|
|
return fos
|
|
}
|
|
|
|
// handlePostField handles POST /field request.
|
|
func (h *Handler) handlePostField(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fieldName, ok := mux.Vars(r)["field"]
|
|
if !ok {
|
|
http.Error(w, "field name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := successResponse{h: h, Name: fieldName}
|
|
|
|
// Decode request.
|
|
var req postFieldRequest
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
err := dec.Decode(&req)
|
|
if err != nil && err != io.EOF {
|
|
resp.write(w, err)
|
|
return
|
|
}
|
|
|
|
// Validate field options.
|
|
if err := req.Options.validate(); err != nil {
|
|
resp.write(w, err)
|
|
return
|
|
}
|
|
|
|
fos := fieldOptionsToFunctionalOpts(req.Options)
|
|
field, err := h.api.CreateField(r.Context(), indexName, fieldName, fos...)
|
|
if _, ok = err.(BadRequestError); ok {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if field != nil {
|
|
resp.CreatedAt = field.CreatedAt()
|
|
} else if _, ok = errors.Cause(err).(ConflictError); ok {
|
|
if field, _ = h.api.Field(r.Context(), indexName, fieldName); field != nil {
|
|
resp.CreatedAt = field.CreatedAt()
|
|
}
|
|
}
|
|
resp.write(w, err)
|
|
}
|
|
|
|
// handlePatchField handles updates to field schema at /index/{index}/field/{field}
|
|
func (h *Handler) handlePatchField(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fieldName, ok := mux.Vars(r)["field"]
|
|
if !ok {
|
|
http.Error(w, "field name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := successResponse{h: h, Name: fieldName}
|
|
|
|
// Decode request.
|
|
var req FieldUpdate
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
err := dec.Decode(&req)
|
|
if err != nil && err != io.EOF {
|
|
resp.write(w, err)
|
|
return
|
|
}
|
|
|
|
err = h.api.UpdateField(r.Context(), indexName, fieldName, req)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
type postFieldRequest struct {
|
|
Options fieldOptions `json:"options"`
|
|
}
|
|
|
|
// fieldOptions tracks FieldOptions. It is made up of pointers to values,
|
|
// and used for input validation.
|
|
type fieldOptions struct {
|
|
Type string `json:"type,omitempty"`
|
|
CacheType *string `json:"cacheType,omitempty"`
|
|
CacheSize *uint32 `json:"cacheSize,omitempty"`
|
|
Min *pql.Decimal `json:"min,omitempty"`
|
|
Max *pql.Decimal `json:"max,omitempty"`
|
|
Scale *int64 `json:"scale,omitempty"`
|
|
Epoch *time.Time `json:"epoch,omitempty"`
|
|
TimeUnit *string `json:"timeUnit,omitempty"`
|
|
TimeQuantum *TimeQuantum `json:"timeQuantum,omitempty"`
|
|
Keys *bool `json:"keys,omitempty"`
|
|
NoStandardView bool `json:"noStandardView,omitempty"`
|
|
ForeignIndex *string `json:"foreignIndex,omitempty"`
|
|
TTL *string `json:"ttl,omitempty"`
|
|
Base *int64 `json:"base,omitempty"`
|
|
}
|
|
|
|
func (o *fieldOptions) validate() error {
|
|
// Pointers to default values.
|
|
defaultCacheType := DefaultCacheType
|
|
defaultCacheSize := uint32(DefaultCacheSize)
|
|
|
|
switch o.Type {
|
|
case FieldTypeSet, "":
|
|
// Because FieldTypeSet is the default, its arguments are
|
|
// not required. Instead, the defaults are applied whenever
|
|
// a value does not exist.
|
|
if o.Type == "" {
|
|
o.Type = FieldTypeSet
|
|
}
|
|
if o.CacheType == nil {
|
|
o.CacheType = &defaultCacheType
|
|
}
|
|
if o.CacheSize == nil {
|
|
o.CacheSize = &defaultCacheSize
|
|
}
|
|
if o.Min != nil {
|
|
return NewBadRequestError(errors.New("min does not apply to field type set"))
|
|
} else if o.Max != nil {
|
|
return NewBadRequestError(errors.New("max does not apply to field type set"))
|
|
} else if o.TimeQuantum != nil {
|
|
return NewBadRequestError(errors.New("timeQuantum does not apply to field type set"))
|
|
} else if o.TTL != nil {
|
|
return NewBadRequestError(errors.New("ttl does not apply to field type set"))
|
|
}
|
|
case FieldTypeInt:
|
|
if o.CacheType != nil {
|
|
return NewBadRequestError(errors.New("cacheType does not apply to field type int"))
|
|
} else if o.CacheSize != nil {
|
|
return NewBadRequestError(errors.New("cacheSize does not apply to field type int"))
|
|
} else if o.TimeQuantum != nil {
|
|
return NewBadRequestError(errors.New("timeQuantum does not apply to field type int"))
|
|
} else if o.TTL != nil {
|
|
return NewBadRequestError(errors.New("ttl does not apply to field type int"))
|
|
}
|
|
case FieldTypeDecimal:
|
|
if o.Scale == nil {
|
|
return NewBadRequestError(errors.New("decimal field requires a scale argument"))
|
|
} else if o.CacheType != nil {
|
|
return NewBadRequestError(errors.New("cacheType does not apply to field type int"))
|
|
} else if o.CacheSize != nil {
|
|
return NewBadRequestError(errors.New("cacheSize does not apply to field type int"))
|
|
} else if o.TimeQuantum != nil {
|
|
return NewBadRequestError(errors.New("timeQuantum does not apply to field type int"))
|
|
} else if o.TTL != nil {
|
|
return NewBadRequestError(errors.New("ttl does not apply to field type int"))
|
|
} else if o.ForeignIndex != nil && o.Type == FieldTypeDecimal {
|
|
return NewBadRequestError(errors.New("decimal field cannot be a foreign key"))
|
|
}
|
|
case FieldTypeTimestamp:
|
|
if o.TimeUnit == nil {
|
|
return NewBadRequestError(errors.New("timestamp field requires a timeUnit argument"))
|
|
} else if !IsValidTimeUnit(*o.TimeUnit) {
|
|
return NewBadRequestError(errors.New("invalid timeUnit argument"))
|
|
} else if o.CacheType != nil {
|
|
return NewBadRequestError(errors.New("cacheType does not apply to field type timestamp"))
|
|
} else if o.CacheSize != nil {
|
|
return NewBadRequestError(errors.New("cacheSize does not apply to field type timestamp"))
|
|
} else if o.TimeQuantum != nil {
|
|
return NewBadRequestError(errors.New("timeQuantum does not apply to field type timestamp"))
|
|
} else if o.TTL != nil {
|
|
return NewBadRequestError(errors.New("ttl does not apply to field type timestamp"))
|
|
} else if o.ForeignIndex != nil {
|
|
return NewBadRequestError(errors.New("timestamp field cannot be a foreign key"))
|
|
}
|
|
case FieldTypeTime:
|
|
if o.CacheType != nil {
|
|
return NewBadRequestError(errors.New("cacheType does not apply to field type time"))
|
|
} else if o.CacheSize != nil {
|
|
return NewBadRequestError(errors.New("cacheSize does not apply to field type time"))
|
|
} else if o.Min != nil {
|
|
return NewBadRequestError(errors.New("min does not apply to field type time"))
|
|
} else if o.Max != nil {
|
|
return NewBadRequestError(errors.New("max does not apply to field type time"))
|
|
} else if o.TimeQuantum == nil {
|
|
return NewBadRequestError(errors.New("timeQuantum is required for field type time"))
|
|
}
|
|
case FieldTypeMutex:
|
|
if o.CacheType == nil {
|
|
o.CacheType = &defaultCacheType
|
|
}
|
|
if o.CacheSize == nil {
|
|
o.CacheSize = &defaultCacheSize
|
|
}
|
|
if o.Min != nil {
|
|
return NewBadRequestError(errors.New("min does not apply to field type mutex"))
|
|
} else if o.Max != nil {
|
|
return NewBadRequestError(errors.New("max does not apply to field type mutex"))
|
|
} else if o.TimeQuantum != nil {
|
|
return NewBadRequestError(errors.New("timeQuantum does not apply to field type mutex"))
|
|
} else if o.TTL != nil {
|
|
return NewBadRequestError(errors.New("ttl does not apply to field type mutex"))
|
|
}
|
|
case FieldTypeBool:
|
|
if o.CacheType != nil {
|
|
return NewBadRequestError(errors.New("cacheType does not apply to field type bool"))
|
|
} else if o.CacheSize != nil {
|
|
return NewBadRequestError(errors.New("cacheSize does not apply to field type bool"))
|
|
} else if o.Min != nil {
|
|
return NewBadRequestError(errors.New("min does not apply to field type bool"))
|
|
} else if o.Max != nil {
|
|
return NewBadRequestError(errors.New("max does not apply to field type bool"))
|
|
} else if o.TimeQuantum != nil {
|
|
return NewBadRequestError(errors.New("timeQuantum does not apply to field type bool"))
|
|
} else if o.Keys != nil {
|
|
return NewBadRequestError(errors.New("keys does not apply to field type bool"))
|
|
} else if o.TTL != nil {
|
|
return NewBadRequestError(errors.New("ttl does not apply to field type bool"))
|
|
} else if o.ForeignIndex != nil {
|
|
return NewBadRequestError(errors.New("bool field cannot be a foreign key"))
|
|
}
|
|
default:
|
|
return errors.Errorf("invalid field type: %s", o.Type)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// handleDeleteField handles DELETE /field request.
|
|
func (h *Handler) handleDeleteField(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
fieldName := mux.Vars(r)["field"]
|
|
|
|
resp := successResponse{h: h}
|
|
err := h.api.DeleteField(r.Context(), indexName, fieldName)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
func (h *Handler) handleGetTransactionList(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
trnsMap, err := h.api.Transactions(r.Context())
|
|
if err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrNodeNotPrimary:
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
default:
|
|
http.Error(w, "problem getting transactions: "+err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Convert the map of transactions to a slice.
|
|
trnsList := make([]*Transaction, len(trnsMap))
|
|
var i int
|
|
for _, v := range trnsMap {
|
|
trnsList[i] = v
|
|
i++
|
|
}
|
|
|
|
// Sort the slice by createdAt.
|
|
sort.Slice(trnsList, func(i, j int) bool {
|
|
return trnsList[i].CreatedAt.Before(trnsList[j].CreatedAt)
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(trnsList); err != nil {
|
|
h.logger.Errorf("encoding GetTransactionList response: %s", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleGetTransactions(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
trnsMap, err := h.api.Transactions(r.Context())
|
|
if err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrNodeNotPrimary:
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
default:
|
|
http.Error(w, "problem getting transactions: "+err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(trnsMap); err != nil {
|
|
h.logger.Errorf("encoding GetTransactions response: %s", err)
|
|
}
|
|
}
|
|
|
|
type TransactionResponse struct {
|
|
Transaction *Transaction `json:"transaction,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func (h *Handler) doTransactionResponse(w http.ResponseWriter, err error, trns *Transaction) {
|
|
if err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrNodeNotPrimary, ErrTransactionExists:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
case ErrTransactionExclusive:
|
|
w.WriteHeader(http.StatusConflict)
|
|
case ErrTransactionNotFound:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
default:
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
var errString string
|
|
if err != nil {
|
|
errString = err.Error()
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err = json.NewEncoder(w).Encode(
|
|
TransactionResponse{Error: errString, Transaction: trns})
|
|
if err != nil {
|
|
h.logger.Errorf("encoding transaction response: %v", err)
|
|
}
|
|
|
|
}
|
|
|
|
func (h *Handler) handleGetTransaction(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
id := mux.Vars(r)["id"]
|
|
trns, err := h.api.GetTransaction(r.Context(), id, false)
|
|
h.doTransactionResponse(w, err, trns)
|
|
}
|
|
|
|
func (h *Handler) handlePostTransaction(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
reqTrns := &Transaction{}
|
|
if err := json.NewDecoder(r.Body).Decode(reqTrns); err != nil || reqTrns.Timeout == 0 {
|
|
if err == nil {
|
|
http.Error(w, "timeout is required and cannot be 0", http.StatusBadRequest)
|
|
} else {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
return
|
|
}
|
|
|
|
id, ok := mux.Vars(r)["id"]
|
|
if !ok {
|
|
id = reqTrns.ID
|
|
}
|
|
|
|
if primary := h.api.PrimaryNode(); h.api.NodeID() == primary.ID {
|
|
trns, err := h.api.StartTransaction(r.Context(), id, reqTrns.Timeout, reqTrns.Exclusive, false)
|
|
h.doTransactionResponse(w, err, trns)
|
|
return
|
|
} else {
|
|
http.Redirect(w, r, primary.URI.Normalize()+"/transaction/"+id, http.StatusSeeOther)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handlePostFinishTransaction(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
id := mux.Vars(r)["id"]
|
|
trns, err := h.api.FinishTransaction(r.Context(), id, false)
|
|
h.doTransactionResponse(w, err, trns)
|
|
}
|
|
|
|
// handleDeleteRemoteAvailableShard handles DELETE /field/{field}/available-shards/{shardID} request.
|
|
func (h *Handler) handleDeleteRemoteAvailableShard(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName := mux.Vars(r)["index"]
|
|
fieldName := mux.Vars(r)["field"]
|
|
shardID, _ := strconv.ParseUint(mux.Vars(r)["shardID"], 10, 64)
|
|
|
|
resp := successResponse{h: h}
|
|
err := h.api.DeleteAvailableShard(r.Context(), indexName, fieldName, shardID)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
// handleGetIndexShardSnapshot handles GET /internal/index/{index}/shard/{shard}/snapshot requests.
|
|
func (h *Handler) handleGetIndexShardSnapshot(w http.ResponseWriter, r *http.Request) {
|
|
indexName := mux.Vars(r)["index"]
|
|
shard, err := strconv.ParseUint(mux.Vars(r)["shard"], 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "Invalid shard parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
rc, err := h.api.IndexShardSnapshot(r.Context(), indexName, shard)
|
|
if err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrIndexNotFound:
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
default:
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
defer rc.Close()
|
|
|
|
// Copy data to response body.
|
|
if _, err := io.CopyBuffer(&passthroughWriter{w}, rc, make([]byte, rbf.PageSize)); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// readQueryRequest parses an query parameters from r.
|
|
func (h *Handler) readQueryRequest(r *http.Request) (*QueryRequest, error) {
|
|
switch r.Header.Get("Content-Type") {
|
|
case "application/x-protobuf":
|
|
return h.readProtobufQueryRequest(r)
|
|
default:
|
|
return h.readURLQueryRequest(r)
|
|
}
|
|
}
|
|
|
|
// passthroughWriter is used to remove non-Writer interfaces from an io.Writer.
|
|
// For example, a writer that implements io.ReaderFrom can change io.Copy() behavior.
|
|
type passthroughWriter struct {
|
|
w io.Writer
|
|
}
|
|
|
|
func (w *passthroughWriter) Write(p []byte) (int, error) {
|
|
return w.w.Write(p)
|
|
}
|
|
|
|
// readProtobufQueryRequest parses query parameters in protobuf from r.
|
|
func (h *Handler) readProtobufQueryRequest(r *http.Request) (*QueryRequest, error) {
|
|
// Slurp the body.
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "reading")
|
|
}
|
|
|
|
qreq := &QueryRequest{}
|
|
err = h.serializer.Unmarshal(body, qreq)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unmarshalling query request")
|
|
}
|
|
return qreq, nil
|
|
}
|
|
|
|
// readURLQueryRequest parses query parameters from URL parameters from r.
|
|
func (h *Handler) readURLQueryRequest(r *http.Request) (*QueryRequest, error) {
|
|
q := r.URL.Query()
|
|
|
|
// Parse query string.
|
|
buf, err := readBody(r)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "reading")
|
|
}
|
|
query := string(buf)
|
|
|
|
// Parse list of shards.
|
|
shards, err := parseUint64Slice(q.Get("shards"))
|
|
if err != nil {
|
|
return nil, errors.New("invalid shard argument")
|
|
}
|
|
|
|
remote := parseBool(q.Get("remote"))
|
|
|
|
// Optional profiling
|
|
profile := false
|
|
profileString := q.Get("profile")
|
|
if profileString != "" {
|
|
profile, err = strconv.ParseBool(q.Get("profile"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid profile argument: '%s' (should be true/false)", profileString)
|
|
}
|
|
}
|
|
|
|
return &QueryRequest{
|
|
Query: query,
|
|
Remote: remote,
|
|
Shards: shards,
|
|
Profile: profile,
|
|
}, nil
|
|
}
|
|
|
|
func parseBool(a string) bool {
|
|
return strings.ToLower(a) == "true"
|
|
}
|
|
|
|
// writeQueryResponse writes the response from the executor to w.
|
|
func (h *Handler) writeQueryResponse(w http.ResponseWriter, r *http.Request, resp *QueryResponse) error {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
w.Header().Set("Content-Type", "application/protobuf")
|
|
return h.writeProtobufQueryResponse(w, resp, headerAcceptRoaringRow(r.Header))
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
return h.writeJSONQueryResponse(w, resp)
|
|
}
|
|
|
|
// writeProtobufQueryResponse writes the response from the executor to w as protobuf.
|
|
func (h *Handler) writeProtobufQueryResponse(w io.Writer, resp *QueryResponse, writeRoaring bool) error {
|
|
serializer := h.serializer
|
|
if writeRoaring {
|
|
serializer = h.roaringSerializer
|
|
}
|
|
if buf, err := serializer.Marshal(resp); err != nil {
|
|
return errors.Wrap(err, "marshalling")
|
|
} else if _, err := w.Write(buf); err != nil {
|
|
return errors.Wrap(err, "writing")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeJSONQueryResponse writes the response from the executor to w as JSON.
|
|
func (h *Handler) writeJSONQueryResponse(w io.Writer, resp *QueryResponse) error {
|
|
return json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func validateProtobufHeader(r *http.Request) (error string, code int) {
|
|
if r.Header.Get("Content-Type") != "application/x-protobuf" {
|
|
return "Unsupported media type", http.StatusUnsupportedMediaType
|
|
}
|
|
if r.Header.Get("Accept") != "application/x-protobuf" {
|
|
return "Not acceptable", http.StatusNotAcceptable
|
|
}
|
|
return
|
|
}
|
|
|
|
// handleGetInternalDebugRBFJSON handles /internal/debug/rbf requests.
|
|
func (h *Handler) handleGetInternalDebugRBFJSON(w http.ResponseWriter, r *http.Request) {
|
|
buf, err := json.MarshalIndent(h.api.RBFDebugInfo(), "", " ")
|
|
if err != nil {
|
|
http.Error(w, "marshal json: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(buf)
|
|
}
|
|
|
|
// handleGetMetricsJSON handles /metrics.json requests, translating text metrics results to more consumable JSON.
|
|
func (h *Handler) handleGetMetricsJSON(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
metrics := make(map[string][]*prom2json.Family)
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
for _, node := range h.api.Hosts(r.Context()) {
|
|
metricsURI := node.URI.String() + "/metrics"
|
|
|
|
// The buffer size of 60 is performance controlling, but we
|
|
// haven't studied what the optimal setting is. It was
|
|
// earlier set to this value to capture all output from
|
|
// prom2json at once. The output got larger recently, so
|
|
// now we handle unlimited size output using a goroutine.
|
|
mfChan := make(chan *dto.MetricFamily, 60)
|
|
errChan := make(chan error)
|
|
go func() {
|
|
err := prom2json.FetchMetricFamilies(metricsURI, mfChan, transport)
|
|
errChan <- err
|
|
}()
|
|
|
|
nodeMetrics := []*prom2json.Family{}
|
|
for mf := range mfChan {
|
|
nodeMetrics = append(nodeMetrics, prom2json.NewFamily(mf))
|
|
}
|
|
err := <-errChan
|
|
if err != nil {
|
|
http.Error(w, "fetching metrics: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
metrics[node.ID] = nodeMetrics
|
|
}
|
|
|
|
err := json.NewEncoder(w).Encode(metrics)
|
|
if err != nil {
|
|
h.logger.Errorf("json write error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetExport handles /export requests.
|
|
func (h *Handler) handleGetExport(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("Accept") {
|
|
case "text/csv":
|
|
h.handleGetExportCSV(w, r)
|
|
default:
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleGetExportCSV(w http.ResponseWriter, r *http.Request) {
|
|
// Parse query parameters.
|
|
q := r.URL.Query()
|
|
index, field := q.Get("index"), q.Get("field")
|
|
|
|
shard, err := strconv.ParseUint(q.Get("shard"), 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid shard", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err = h.api.ExportCSV(r.Context(), index, field, shard, w); err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrFragmentNotFound:
|
|
break
|
|
case ErrClusterDoesNotOwnShard:
|
|
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
|
default:
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// handleGetFragmentNodes handles /internal/fragment/nodes requests.
|
|
func (h *Handler) handleGetFragmentNodes(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
q := r.URL.Query()
|
|
index := q.Get("index")
|
|
|
|
// Read shard parameter.
|
|
shard, err := strconv.ParseUint(q.Get("shard"), 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "shard should be an unsigned integer", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Retrieve fragment owner nodes.
|
|
nodes, err := h.api.ShardNodes(r.Context(), index, shard)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Write to response.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(nodes); err != nil {
|
|
h.logger.Errorf("json write error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetPartitionNodes handles /internal/partition/nodes requests.
|
|
func (h *Handler) handleGetPartitionNodes(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
q := r.URL.Query()
|
|
|
|
// Read partition parameter.
|
|
partitionID, err := strconv.ParseInt(q.Get("partition"), 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "shard should be an unsigned integer", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Retrieve fragment owner nodes.
|
|
nodes, err := h.api.PartitionNodes(r.Context(), int(partitionID))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Write to response.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(nodes); err != nil {
|
|
h.logger.Errorf("json write error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetNodes handles /internal/nodes requests.
|
|
func (h *Handler) handleGetNodes(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
// Retrieve all nodes.
|
|
nodes := h.api.Hosts(r.Context())
|
|
|
|
// Write to response.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(nodes); err != nil {
|
|
h.logger.Errorf("json write error: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetFragmentBlockData handles GET /internal/fragment/block/data requests.
|
|
func (h *Handler) handleGetFragmentBlockData(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "fragment blocks feature removed", http.StatusNotFound)
|
|
}
|
|
|
|
// handleGetFragmentBlocks handles GET /internal/fragment/blocks requests.
|
|
func (h *Handler) handleGetFragmentBlocks(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "fragment blocks feature removed", http.StatusNotFound)
|
|
}
|
|
|
|
// handleGetFragmentData handles GET /internal/fragment/data requests.
|
|
func (h *Handler) handleGetFragmentData(w http.ResponseWriter, r *http.Request) {
|
|
// Read shard parameter.
|
|
q := r.URL.Query()
|
|
shard, err := strconv.ParseUint(q.Get("shard"), 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "shard required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// Retrieve fragment data from holder.
|
|
f, err := h.api.FragmentData(r.Context(), q.Get("index"), q.Get("field"), q.Get("view"), shard)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
// Stream fragment to response body.
|
|
if _, err := f.WriteTo(w); err != nil {
|
|
h.logger.Errorf("error streaming fragment data: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetTranslateData handles GET /internal/translate/data requests.
|
|
func (h *Handler) handleGetTranslateData(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
|
|
// Perform field translation copy, if field specified.
|
|
if fieldName := q.Get("field"); fieldName != "" {
|
|
// Retrieve field data from holder.
|
|
p, err := h.api.FieldTranslateData(r.Context(), q.Get("index"), fieldName)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
// Stream translate data to response body.
|
|
if _, err := p.WriteTo(w); err != nil {
|
|
h.logger.Errorf("error streaming translation data: %s", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Otherwise read partition parameter for index translation copy.
|
|
partition, err := strconv.ParseUint(q.Get("partition"), 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "partition or field required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Retrieve partition data from holder.
|
|
p, err := h.api.TranslateData(r.Context(), q.Get("index"), int(partition))
|
|
if redir, ok := err.(RedirectError); ok {
|
|
newURL := *r.URL
|
|
newURL.Host = redir.HostPort
|
|
http.Redirect(w, r, newURL.String(), http.StatusSeeOther)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
// Stream translate partition to response body.
|
|
if _, err := p.WriteTo(w); err != nil {
|
|
h.logger.Errorf("error streaming translation data: %s", err)
|
|
}
|
|
}
|
|
|
|
// handleGetVersion handles /version requests.
|
|
func (h *Handler) handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err := json.NewEncoder(w).Encode(struct {
|
|
Version string `json:"version"`
|
|
}{
|
|
Version: h.api.Version(),
|
|
})
|
|
if err != nil {
|
|
h.logger.Errorf("write version response error: %s", err)
|
|
}
|
|
}
|
|
|
|
// QueryResult types.
|
|
const (
|
|
QueryResultTypeRow uint32 = iota
|
|
QueryResultTypePairs
|
|
QueryResultTypeUint64
|
|
)
|
|
|
|
// parseUint64Slice returns a slice of uint64s from a comma-delimited string.
|
|
func parseUint64Slice(s string) ([]uint64, error) {
|
|
var a []uint64
|
|
for _, str := range strings.Split(s, ",") {
|
|
// Ignore blanks.
|
|
if str == "" {
|
|
continue
|
|
}
|
|
|
|
// Parse number.
|
|
num, err := strconv.ParseUint(str, 10, 64)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "parsing int")
|
|
}
|
|
a = append(a, num)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (h *Handler) handleRecalculateCaches(w http.ResponseWriter, r *http.Request) {
|
|
err := h.api.RecalculateCaches(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "recalculating caches: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) handlePostClusterMessage(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
// Verify that request is only communicating over protobufs.
|
|
if r.Header.Get("Content-Type") != "application/x-protobuf" {
|
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
|
return
|
|
}
|
|
err := h.api.ClusterMessage(r.Context(), r.Body)
|
|
if err != nil {
|
|
switch err := err.(type) {
|
|
case MessageProcessingError:
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
default:
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(defaultClusterMessageResponse{}); err != nil {
|
|
h.logger.Errorf("response encoding error: %s", err)
|
|
}
|
|
}
|
|
|
|
type defaultClusterMessageResponse struct{}
|
|
|
|
func (h *Handler) handlePostTranslateData(w http.ResponseWriter, r *http.Request) {
|
|
// Parse offsets for all indexes and fields from POST body.
|
|
offsets := make(TranslateOffsetMap)
|
|
if err := json.NewDecoder(r.Body).Decode(&offsets); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Stream all translation data.
|
|
rd, err := h.api.GetTranslateEntryReader(r.Context(), offsets)
|
|
if errors.Cause(err) == ErrNotImplemented {
|
|
http.Error(w, err.Error(), http.StatusNotImplemented)
|
|
return
|
|
} else if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rd.Close()
|
|
|
|
// Flush header so client can continue.
|
|
w.WriteHeader(http.StatusOK)
|
|
w.(http.Flusher).Flush()
|
|
|
|
// Copy from reader to client until store or client disconnect.
|
|
enc := json.NewEncoder(w)
|
|
for {
|
|
// Read from store.
|
|
var entry TranslateEntry
|
|
if err := rd.ReadEntry(&entry); err == io.EOF {
|
|
return
|
|
} else if err != nil {
|
|
h.logger.Errorf("http: translate store read error: %s", err)
|
|
return
|
|
}
|
|
|
|
// Write to response & flush.
|
|
if err := enc.Encode(&entry); err != nil {
|
|
return
|
|
}
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
}
|
|
|
|
type queryValidationSpec struct {
|
|
required []string
|
|
args map[string]struct{}
|
|
}
|
|
|
|
func queryValidationSpecRequired(requiredArgs ...string) *queryValidationSpec {
|
|
args := map[string]struct{}{}
|
|
for _, arg := range requiredArgs {
|
|
args[arg] = struct{}{}
|
|
}
|
|
|
|
return &queryValidationSpec{
|
|
required: requiredArgs,
|
|
args: args,
|
|
}
|
|
}
|
|
|
|
func (s *queryValidationSpec) Optional(args ...string) *queryValidationSpec {
|
|
for _, arg := range args {
|
|
s.args[arg] = struct{}{}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s queryValidationSpec) validate(query url.Values) error {
|
|
for _, req := range s.required {
|
|
if query.Get(req) == "" {
|
|
return errors.Errorf("%s is required", req)
|
|
}
|
|
}
|
|
for k := range query {
|
|
if _, ok := s.args[k]; !ok {
|
|
return errors.Errorf("%s is not a valid argument", k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ClientOption func(client *http.Client, dialer *net.Dialer) *http.Client
|
|
|
|
func ClientResponseHeaderTimeoutOption(dur time.Duration) ClientOption {
|
|
return func(client *http.Client, dialer *net.Dialer) *http.Client {
|
|
client.Transport.(*http.Transport).ResponseHeaderTimeout = dur
|
|
return client
|
|
}
|
|
}
|
|
|
|
func ClientDialTimeoutOption(dur time.Duration) ClientOption {
|
|
return func(client *http.Client, dialer *net.Dialer) *http.Client {
|
|
dialer.Timeout = dur
|
|
return client
|
|
}
|
|
}
|
|
|
|
func GetHTTPClient(t *tls.Config, opts ...ClientOption) *http.Client {
|
|
dialer := &net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 15 * time.Second,
|
|
DualStack: true,
|
|
}
|
|
transport := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: dialer.DialContext,
|
|
MaxIdleConns: 1000,
|
|
MaxIdleConnsPerHost: 200,
|
|
IdleConnTimeout: 20 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
if t != nil {
|
|
transport.TLSClientConfig = t
|
|
}
|
|
|
|
client := &http.Client{Transport: transport}
|
|
for _, opt := range opts {
|
|
client = opt(client, dialer)
|
|
}
|
|
return client
|
|
}
|
|
|
|
// handlePostImportAtomicRecord handles /import-atomic-record requests
|
|
func (h *Handler) handlePostImportAtomicRecord(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Verify that request is only communicating over protobufs.
|
|
if error, code := validateProtobufHeader(r); error != "" {
|
|
http.Error(w, error, code)
|
|
return
|
|
}
|
|
|
|
// Read entire body.
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// Unmarshal request based on field type.
|
|
|
|
q := r.URL.Query()
|
|
sLoss := q.Get("simPowerLossAfter")
|
|
loss := 0
|
|
if sLoss != "" {
|
|
l, err := strconv.ParseInt(sLoss, 10, 64)
|
|
loss = int(l)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
}
|
|
opt := func(o *ImportOptions) error {
|
|
o.SimPowerLossAfter = loss
|
|
return nil
|
|
}
|
|
|
|
req := &AtomicRecord{}
|
|
if err := h.serializer.Unmarshal(body, req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
qcx := h.api.Txf().NewQcx()
|
|
err = h.api.ImportAtomicRecord(r.Context(), qcx, req, opt)
|
|
if err == nil {
|
|
err = qcx.Finish()
|
|
} else {
|
|
qcx.Abort()
|
|
}
|
|
if err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrClusterDoesNotOwnShard, ErrPreconditionFailed:
|
|
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
|
case ErrBSIGroupValueTooLow, ErrBSIGroupValueTooHigh, ErrDecimalOutOfRange:
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
default:
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Write response.
|
|
_, err = w.Write(importOk)
|
|
if err != nil {
|
|
h.logger.Errorf("writing import response: %v", err)
|
|
}
|
|
}
|
|
|
|
// handlePostImport handles /import requests.
|
|
func (h *Handler) handlePostImport(w http.ResponseWriter, r *http.Request) {
|
|
// Verify that request is only communicating over protobufs.
|
|
if err, code := validateProtobufHeader(r); err != "" {
|
|
http.Error(w, err, code)
|
|
return
|
|
}
|
|
|
|
// Get index and field type to determine how to handle the
|
|
// import data.
|
|
indexName := mux.Vars(r)["index"]
|
|
index, err := h.api.Index(r.Context(), indexName)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrIndexNotFound {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
} else {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
fieldName := mux.Vars(r)["field"]
|
|
field := index.Field(fieldName)
|
|
if field == nil {
|
|
http.Error(w, ErrFieldNotFound.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// If the clear flag is true, treat the import as clear bits.
|
|
q := r.URL.Query()
|
|
doClear := q.Get("clear") == "true"
|
|
doIgnoreKeyCheck := q.Get("ignoreKeyCheck") == "true"
|
|
|
|
opts := []ImportOption{
|
|
OptImportOptionsClear(doClear),
|
|
OptImportOptionsIgnoreKeyCheck(doIgnoreKeyCheck),
|
|
}
|
|
|
|
// Read entire body.
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// Unmarshal request based on field type.
|
|
if field.Type() == FieldTypeInt || field.Type() == FieldTypeDecimal || field.Type() == FieldTypeTimestamp {
|
|
// Field type: Int
|
|
// Marshal into request object.
|
|
req := &ImportValueRequest{}
|
|
if err := h.serializer.Unmarshal(body, req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
qcx := h.api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
|
|
if err := h.api.ImportValue(r.Context(), qcx, req, opts...); err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrClusterDoesNotOwnShard, ErrPreconditionFailed:
|
|
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
|
case ErrBSIGroupValueTooLow, ErrBSIGroupValueTooHigh, ErrDecimalOutOfRange:
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
default:
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return
|
|
}
|
|
err := qcx.Finish()
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("error in qcx.Finish(): '%v'", err.Error()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
// Field type: set, time, mutex
|
|
// Marshal into request object.
|
|
req := &ImportRequest{}
|
|
if err := h.serializer.Unmarshal(body, req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
qcx := h.api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
|
|
if err := h.api.Import(r.Context(), qcx, req, opts...); err != nil {
|
|
switch errors.Cause(err) {
|
|
case ErrClusterDoesNotOwnShard, ErrPreconditionFailed:
|
|
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
|
case ErrBSIGroupValueTooLow, ErrBSIGroupValueTooHigh, ErrDecimalOutOfRange:
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
default:
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
err := qcx.Finish()
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("error in qcx.Finish() on set,time,mutex: '%v'", err.Error()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Write response.
|
|
_, err = w.Write(importOk)
|
|
if err != nil {
|
|
h.logger.Errorf("writing import response: %v", err)
|
|
}
|
|
}
|
|
|
|
// handleGetMutexCheck handles /mutex-check requests.
|
|
func (h *Handler) handleGetMutexCheck(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
// Get index and field type to determine how to handle the
|
|
// import data.
|
|
indexName, fieldName := mux.Vars(r)["index"], mux.Vars(r)["field"]
|
|
q := r.URL.Query()
|
|
limit := 0
|
|
details := q.Get("details") == "true"
|
|
limitStr := q.Get("limit")
|
|
if limitStr != "" {
|
|
var err error
|
|
limit, err = strconv.Atoi(limitStr)
|
|
if err != nil {
|
|
http.Error(w, "limit must be numeric", http.StatusBadRequest)
|
|
}
|
|
}
|
|
qcx := h.api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
out, err := h.api.MutexCheck(r.Context(), qcx, indexName, fieldName, details, limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
outBytes, err := json.Marshal(out)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("marshalling response: %v", err), http.StatusInternalServerError)
|
|
}
|
|
_, err = w.Write(outBytes)
|
|
if err != nil {
|
|
h.logger.Errorf("writing mutex-check response: %v", err)
|
|
}
|
|
}
|
|
|
|
// handleInternalGetMutexCheck handles internal (non-forwarding )/mutex-check requests.
|
|
func (h *Handler) handleInternalGetMutexCheck(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
// Get index and field type to determine how to handle the
|
|
// import data.
|
|
indexName, fieldName := mux.Vars(r)["index"], mux.Vars(r)["field"]
|
|
q := r.URL.Query()
|
|
limit := 0
|
|
details := q.Get("details") == "true"
|
|
limitStr := q.Get("limit")
|
|
if limitStr != "" {
|
|
var err error
|
|
limit, err = strconv.Atoi(limitStr)
|
|
if err != nil {
|
|
http.Error(w, "limit must be numeric", http.StatusBadRequest)
|
|
}
|
|
}
|
|
qcx := h.api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
out, err := h.api.MutexCheckNode(r.Context(), qcx, indexName, fieldName, details, limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
outBytes, err := json.Marshal(out)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("marshalling response: %v", err), http.StatusInternalServerError)
|
|
}
|
|
_, err = w.Write(outBytes)
|
|
if err != nil {
|
|
h.logger.Errorf("writing mutex-check response: %v", err)
|
|
}
|
|
}
|
|
|
|
// handlePostImportRoaring
|
|
func (h *Handler) handlePostImportRoaring(w http.ResponseWriter, r *http.Request) {
|
|
// Verify that request is only communicating over protobufs.
|
|
if error, code := validateProtobufHeader(r); error != "" {
|
|
http.Error(w, error, code)
|
|
return
|
|
}
|
|
|
|
// Get index and field type to determine how to handle the
|
|
// import data.
|
|
indexName := mux.Vars(r)["index"]
|
|
fieldName := mux.Vars(r)["field"]
|
|
|
|
q := r.URL.Query()
|
|
remoteStr := q.Get("remote")
|
|
var remote bool
|
|
if remoteStr == "true" {
|
|
remote = true
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
// Read entire body.
|
|
span, _ := tracing.StartSpanFromContext(ctx, "io.ReadAll-Body")
|
|
body, err := readBody(r)
|
|
span.LogKV("bodySize", len(body))
|
|
span.Finish()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
req := &ImportRoaringRequest{}
|
|
span, _ = tracing.StartSpanFromContext(ctx, "Unmarshal")
|
|
err = h.serializer.Unmarshal(body, req)
|
|
span.Finish()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
urlVars := mux.Vars(r)
|
|
shard, err := strconv.ParseUint(urlVars["shard"], 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "shard should be an unsigned integer", http.StatusBadRequest)
|
|
return
|
|
}
|
|
resp := &ImportResponse{}
|
|
// TODO give meaningful stats for import
|
|
err = h.api.ImportRoaring(ctx, indexName, fieldName, shard, remote, req)
|
|
if err != nil {
|
|
resp.Err = err.Error()
|
|
if _, ok := err.(BadRequestError); ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
} else if _, ok := err.(NotFoundError); ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
} else if _, ok := err.(PreconditionFailedError); ok {
|
|
w.WriteHeader(http.StatusPreconditionFailed)
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// Marshal response object.
|
|
buf, err := h.serializer.Marshal(resp)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("marshal import-roaring response: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Write response.
|
|
_, err = w.Write(buf)
|
|
if err != nil {
|
|
h.logger.Errorf("writing import-roaring response: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// handlePostShardImportRoaring takes data for multiple fields for a
|
|
// particular shard and imports it all in a single transaction. It was
|
|
// developed in the post-RBF world and should probably ultimately
|
|
// replace most of the other import endpoints.
|
|
func (h *Handler) handlePostShardImportRoaring(w http.ResponseWriter, r *http.Request) {
|
|
// Verify that request is only communicating over protobufs.
|
|
if error, code := validateProtobufHeader(r); error != "" {
|
|
http.Error(w, error, code)
|
|
return
|
|
}
|
|
|
|
// Get index and field type to determine how to handle the
|
|
// import data.
|
|
indexName := mux.Vars(r)["index"]
|
|
|
|
ctx := r.Context()
|
|
|
|
// Read entire body.
|
|
span, _ := tracing.StartSpanFromContext(ctx, "io.ReadAll-Body")
|
|
body, err := readBody(r)
|
|
span.LogKV("bodySize", len(body))
|
|
span.Finish()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
req := &ImportRoaringShardRequest{}
|
|
span, _ = tracing.StartSpanFromContext(ctx, "Unmarshal")
|
|
err = h.serializer.Unmarshal(body, req)
|
|
span.Finish()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
urlVars := mux.Vars(r)
|
|
shard, err := strconv.ParseUint(urlVars["shard"], 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "shard should be an unsigned integer", http.StatusBadRequest)
|
|
return
|
|
}
|
|
resp := &ImportResponse{}
|
|
// TODO give meaningful stats for import
|
|
err = h.api.ImportRoaringShard(ctx, indexName, shard, req)
|
|
if err != nil {
|
|
resp.Err = err.Error()
|
|
if errors.Is(err, ErrIndexNotFound) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
} else if errors.As(err, &BadRequestError{}) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
} else if _, ok := errors.Cause(err).(NotFoundError); ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
} else if errors.As(err, &PreconditionFailedError{}) {
|
|
w.WriteHeader(http.StatusPreconditionFailed)
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// Marshal response object.
|
|
buf, err := h.serializer.Marshal(resp)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("marshal shard-import-roaring response: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Write response.
|
|
_, err = w.Write(buf)
|
|
if err != nil {
|
|
h.logger.Errorf("writing shard-import-roaring response: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handlePostTranslateKeys(w http.ResponseWriter, r *http.Request) {
|
|
// Verify that request is only communicating over protobufs.
|
|
if r.Header.Get("Content-Type") != "application/x-protobuf" {
|
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
|
return
|
|
} else if r.Header.Get("Accept") != "application/x-protobuf" {
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
buf, err := h.api.TranslateKeys(r.Context(), r.Body)
|
|
switch errors.Cause(err) {
|
|
case nil:
|
|
// Write response.
|
|
if _, err = w.Write(buf); err != nil {
|
|
h.logger.Errorf("writing translate keys response: %v", err)
|
|
}
|
|
|
|
case ErrTranslatingKeyNotFound:
|
|
http.Error(w, fmt.Sprintf("translate keys: %v", err), http.StatusNotFound)
|
|
|
|
case ErrTranslateStoreReadOnly:
|
|
http.Error(w, fmt.Sprintf("translate keys: %v", err), http.StatusPreconditionFailed)
|
|
|
|
default:
|
|
http.Error(w, fmt.Sprintf("translate keys: %v", err), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handlePostTranslateIDs(w http.ResponseWriter, r *http.Request) {
|
|
// Verify that request is only communicating over protobufs.
|
|
if r.Header.Get("Content-Type") != "application/x-protobuf" {
|
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
|
return
|
|
} else if r.Header.Get("Accept") != "application/x-protobuf" {
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
buf, err := h.api.TranslateIDs(r.Context(), r.Body)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("translate ids: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Write response.
|
|
_, err = w.Write(buf)
|
|
if err != nil {
|
|
h.logger.Errorf("writing translate keys response: %v", err)
|
|
}
|
|
}
|
|
|
|
// Read entire request body.
|
|
func readBody(r *http.Request) ([]byte, error) {
|
|
var contentLength int64 = bytes.MinRead
|
|
if r.ContentLength > 0 {
|
|
contentLength = r.ContentLength
|
|
}
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, 1+contentLength))
|
|
if _, err := buf.ReadFrom(r.Body); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (h *Handler) handlePostTranslateFieldDB(w http.ResponseWriter, r *http.Request) {
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fieldName, ok := mux.Vars(r)["field"]
|
|
if !ok {
|
|
http.Error(w, "field name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
bd, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
br := bytes.NewReader(bd)
|
|
|
|
err = h.api.TranslateFieldDB(r.Context(), indexName, fieldName, br)
|
|
resp := successResponse{h: h, Name: fieldName}
|
|
resp.check(err)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
func (h *Handler) handlePostTranslateIndexDB(w http.ResponseWriter, r *http.Request) {
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
partitionArg, ok := mux.Vars(r)["partition"]
|
|
if !ok {
|
|
http.Error(w, "partition is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
partition, err := strconv.ParseUint(partitionArg, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "bad partition", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
bd, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
br := bytes.NewReader(bd)
|
|
err = h.api.TranslateIndexDB(r.Context(), indexName, int(partition), br)
|
|
resp := successResponse{h: h, Name: indexName}
|
|
resp.check(err)
|
|
resp.write(w, err)
|
|
}
|
|
|
|
func (h *Handler) handleFindOrCreateKeys(w http.ResponseWriter, r *http.Request, requireField bool, create bool) {
|
|
// Verify input and output types
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
|
return
|
|
}
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
var indexName, fieldName string
|
|
var keys []string
|
|
err := func() error {
|
|
var ok bool
|
|
indexName, ok = mux.Vars(r)["index"]
|
|
if !ok {
|
|
return errors.New("index name is required")
|
|
}
|
|
|
|
if requireField {
|
|
fieldName, ok = mux.Vars(r)["field"]
|
|
if !ok {
|
|
return errors.New("field name is required")
|
|
}
|
|
}
|
|
|
|
bd, err := readBody(r)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read body: %v", err)
|
|
}
|
|
|
|
err = json.Unmarshal(bd, &keys)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode request: %v", err)
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
var translations map[string]uint64
|
|
switch {
|
|
case requireField && create:
|
|
translations, err = h.api.CreateFieldKeys(r.Context(), indexName, fieldName, keys...)
|
|
case requireField && !create:
|
|
translations, err = h.api.FindFieldKeys(r.Context(), indexName, fieldName, keys...)
|
|
case !requireField && create:
|
|
translations, err = h.api.CreateIndexKeys(r.Context(), indexName, keys...)
|
|
case !requireField && !create:
|
|
translations, err = h.api.FindIndexKeys(r.Context(), indexName, keys...)
|
|
}
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("translating keys: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
data, err := json.Marshal(translations)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("encoding response: %v", err), http.StatusInternalServerError)
|
|
}
|
|
_, err = w.Write(data)
|
|
if err != nil {
|
|
h.logger.Printf("writing CreateFieldKeys response: %v", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleFindIndexKeys(w http.ResponseWriter, r *http.Request) {
|
|
h.handleFindOrCreateKeys(w, r, false, false)
|
|
}
|
|
|
|
func (h *Handler) handleFindFieldKeys(w http.ResponseWriter, r *http.Request) {
|
|
h.handleFindOrCreateKeys(w, r, true, false)
|
|
}
|
|
|
|
func (h *Handler) handleCreateIndexKeys(w http.ResponseWriter, r *http.Request) {
|
|
h.handleFindOrCreateKeys(w, r, false, true)
|
|
}
|
|
|
|
func (h *Handler) handleCreateFieldKeys(w http.ResponseWriter, r *http.Request) {
|
|
h.handleFindOrCreateKeys(w, r, true, true)
|
|
}
|
|
|
|
func (h *Handler) handleMatchField(w http.ResponseWriter, r *http.Request) {
|
|
// Verify output type.
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fieldName, ok := mux.Vars(r)["field"]
|
|
if !ok {
|
|
http.Error(w, "field name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
bd, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matches, err := h.api.MatchField(r.Context(), indexName, fieldName, string(bd))
|
|
if err != nil {
|
|
http.Error(w, "failed to match pattern", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
err = json.NewEncoder(w).Encode(matches)
|
|
if err != nil {
|
|
http.Error(w, "encoding result", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleReserveIDs(w http.ResponseWriter, r *http.Request) {
|
|
// Verify input and output types
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
|
return
|
|
}
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
bd, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var req IDAllocReserveRequest
|
|
req.Offset = ^uint64(0)
|
|
err = json.Unmarshal(bd, &req)
|
|
if err != nil {
|
|
http.Error(w, "failed to decode request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ids, err := h.api.ReserveIDs(req.Key, req.Session, req.Offset, req.Count)
|
|
if err != nil {
|
|
var esync ErrIDOffsetDesync
|
|
if errors.As(err, &esync) {
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusConflict)
|
|
err = json.NewEncoder(w).Encode(struct {
|
|
ErrIDOffsetDesync
|
|
Err string `json:"error"`
|
|
}{
|
|
ErrIDOffsetDesync: esync,
|
|
Err: err.Error(),
|
|
})
|
|
if err != nil {
|
|
h.logger.Debugf("failed to send desync error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
http.Error(w, fmt.Sprintf("reserving IDs: %v", err.Error()), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Type", "application/json")
|
|
err = json.NewEncoder(w).Encode(ids)
|
|
if err != nil {
|
|
http.Error(w, "encoding result", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleCommitIDs(w http.ResponseWriter, r *http.Request) {
|
|
// Verify input and output types
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
|
return
|
|
} else if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "Not acceptable", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
bd, err := readBody(r)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var req IDAllocCommitRequest
|
|
err = json.Unmarshal(bd, &req)
|
|
if err != nil {
|
|
http.Error(w, "failed to decode request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err = h.api.CommitIDs(req.Key, req.Session, req.Count)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("committing IDs: %v", err.Error()), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) handleResetIDAlloc(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptType(r.Header, "text", "plain") {
|
|
http.Error(w, "text/plain is not an acceptable response type", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err := h.api.ResetIDAlloc(indexName)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("resetting ID allocation: %v", err.Error()), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK")) //nolint:errcheck
|
|
}
|
|
|
|
func (h *Handler) handleIDAllocData(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("Content-Type", "application/octet-stream")
|
|
if err := h.api.WriteIDAllocDataTo(w); err != nil {
|
|
http.Error(w, fmt.Sprintf("writeing id allocation data: %v", err.Error()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleRestoreIDAlloc(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.api.RestoreIDAlloc(r.Body); err != nil {
|
|
http.Error(w, fmt.Sprintf("restoring id allocation: %v", err.Error()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK")) //nolint:errcheck
|
|
}
|
|
func (h *Handler) handlePostRestore(w http.ResponseWriter, r *http.Request) {
|
|
indexName, ok := mux.Vars(r)["index"]
|
|
if !ok {
|
|
http.Error(w, "index name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
shardID, ok := mux.Vars(r)["shardID"]
|
|
if !ok {
|
|
http.Error(w, "shardID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
shard, err := strconv.ParseUint(shardID, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to parse shard %v %v err:%v", indexName, shardID, err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
ctx := context.Background()
|
|
//validate shard for this node
|
|
err = h.api.RestoreShard(ctx, indexName, shard, r.Body)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to restore shard %v %v err:%v", indexName, shard, err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK")) //nolint:errcheck
|
|
}
|
|
|
|
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
if h.auth == nil {
|
|
http.Error(w, "", http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
h.auth.Login(w, r)
|
|
}
|
|
|
|
func (h *Handler) handleRedirect(w http.ResponseWriter, r *http.Request) {
|
|
if h.auth == nil {
|
|
http.Error(w, "", http.StatusNoContent)
|
|
return
|
|
}
|
|
h.auth.Redirect(w, r)
|
|
}
|
|
|
|
// handleOAuthConfig handles requests for a cleaned version of our oAuthConfig. We
|
|
// use this endpoint /internal/oauth-config in the `featurebase auth-token`
|
|
// subcommand to create a RedirectURL on the fly.
|
|
func (h *Handler) handleOAuthConfig(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
if h.auth == nil {
|
|
http.Error(w, "auth not enabled: no OAuthConfig", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
config := h.auth.CleanOAuthConfig()
|
|
if err := json.NewEncoder(w).Encode(config); err != nil {
|
|
h.logger.Errorf("writing oauth-config info: %s", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleCheckAuthentication(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
if h.auth == nil {
|
|
http.Error(w, "", http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
access, refresh := getTokens(r)
|
|
uinfo, err := h.auth.Authenticate(access, refresh)
|
|
if uinfo == nil || err != nil {
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
// just in case it got refreshed
|
|
h.auth.SetCookie(w, uinfo.Token, uinfo.RefreshToken, uinfo.Expiry)
|
|
|
|
w.Header().Add("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK")) //nolint:errcheck
|
|
}
|
|
|
|
func (h *Handler) handleUserInfo(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
if h.auth == nil {
|
|
http.Error(w, "", http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
access, refresh := getTokens(r)
|
|
uinfo, err := h.auth.Authenticate(access, refresh)
|
|
if err != nil {
|
|
h.logger.Errorf("error authenticating: %v", err)
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
// just in case it got refreshed
|
|
h.auth.SetCookie(w, uinfo.Token, uinfo.RefreshToken, uinfo.Expiry)
|
|
|
|
if err := json.NewEncoder(w).Encode(uinfo); err != nil {
|
|
h.logger.Errorf("writing user info: %s", err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
if h.auth == nil {
|
|
http.Error(w, "", http.StatusNoContent)
|
|
return
|
|
}
|
|
h.auth.Logout(w, r)
|
|
}
|
|
|
|
// getTokens gets the access and refresh tokens from the request,
|
|
// returning empty strings if they aren't in the request.
|
|
func getTokens(r *http.Request) (string, string) {
|
|
var access, refresh string
|
|
if token, ok := r.Header["Authorization"]; ok && len(token) > 0 {
|
|
parts := strings.Split(token[0], "Bearer ")
|
|
if len(parts) >= 2 {
|
|
access = parts[1]
|
|
}
|
|
}
|
|
|
|
if token, ok := r.Header[authn.RefreshHeaderName]; ok && len(token) > 0 {
|
|
refresh = token[0]
|
|
}
|
|
|
|
if access == "" {
|
|
accessCookie, err := r.Cookie(authn.AccessCookieName)
|
|
if err != nil {
|
|
return access, refresh
|
|
}
|
|
access = accessCookie.Value
|
|
}
|
|
|
|
if refresh == "" {
|
|
refreshCookie, err := r.Cookie(authn.RefreshCookieName)
|
|
if err != nil {
|
|
return access, refresh
|
|
}
|
|
refresh = refreshCookie.Value
|
|
}
|
|
|
|
return access, refresh
|
|
}
|
|
|
|
func (h *Handler) handlePostDirective(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
body := r.Body
|
|
defer body.Close()
|
|
|
|
d := &dax.Directive{}
|
|
if err := json.NewDecoder(body).Decode(d); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.api.Directive(r.Context(), d); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// POST /snapshot/shard-data
|
|
func (h *Handler) handlePostSnapshotShardData(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
body := r.Body
|
|
defer body.Close()
|
|
|
|
req := &dax.SnapshotShardDataRequest{}
|
|
if err := json.NewDecoder(body).Decode(req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.api.SnapshotShardData(r.Context(), req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// POST /snapshot/table-keys
|
|
func (h *Handler) handlePostSnapshotTableKeys(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
body := r.Body
|
|
defer body.Close()
|
|
|
|
req := &dax.SnapshotTableKeysRequest{}
|
|
if err := json.NewDecoder(body).Decode(req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.api.SnapshotTableKeys(r.Context(), req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// POST /snapshot/field-keys
|
|
func (h *Handler) handlePostSnapshotFieldKeys(w http.ResponseWriter, r *http.Request) {
|
|
if !validHeaderAcceptJSON(r.Header) {
|
|
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
|
|
body := r.Body
|
|
defer body.Close()
|
|
|
|
req := &dax.SnapshotFieldKeysRequest{}
|
|
if err := json.NewDecoder(body).Decode(req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.api.SnapshotFieldKeys(r.Context(), req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// GET /health
|
|
func (h *Handler) handleGetHealth(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|