feat: add onboarding defaults

pull/10616/head
Kelvin Wang 2018-09-12 15:24:17 -04:00
parent 1573ff6c24
commit 3552af6386
10 changed files with 601 additions and 3 deletions

View File

@ -98,6 +98,11 @@ func (c *Client) initialize(ctx context.Context) error {
return err
}
// Always create Onboarding bucket.
if err := c.initializeOnboarding(ctx, tx); err != nil {
return err
}
// Always create Source bucket.
if err := c.initializeSources(ctx, tx); err != nil {
return err

136
bolt/onboarding.go Normal file
View File

@ -0,0 +1,136 @@
package bolt
import (
"context"
bolt "github.com/coreos/bbolt"
"github.com/influxdata/platform"
)
var onboardingBucket = []byte("onboardingv1")
var onboardingKey = []byte("onboarding_key")
var _ platform.OnboardingService = (*Client)(nil)
func (c *Client) initializeOnboarding(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(onboardingBucket)); err != nil {
return err
}
return nil
}
// IsOnboarding checks onboardingBucket
// to see if the onboarding key is true.
func (c *Client) IsOnboarding(ctx context.Context) (isOnboarding bool, err error) {
err = c.db.View(func(tx *bolt.Tx) error {
result := tx.Bucket(onboardingBucket).Get(onboardingKey)
isOnboarding = len(result) == 0
return nil
})
return isOnboarding, err
}
// PutOnboardingStatus will update the flag,
// so future onboarding request will be denied.
func (c *Client) PutOnboardingStatus(ctx context.Context, v bool) error {
if v {
return c.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket(onboardingBucket).Put(onboardingKey, []byte{0x1})
})
}
return nil
}
// Generate OnboardingResults from onboarding request,
// update db so this request will be disabled for the second run.
func (c *Client) Generate(ctx context.Context, req *platform.OnboardingRequest) (*platform.OnboardingResults, error) {
isOnboarding, err := c.IsOnboarding(ctx)
if err != nil {
return nil, err
}
if !isOnboarding {
return nil, &platform.Error{
Code: platform.EConflict,
Msg: "onboarding has already been completed",
}
}
if req.Password == "" {
return nil, &platform.Error{
Code: platform.EEmptyValue,
Msg: "password is empty",
}
}
if req.User == "" {
return nil, &platform.Error{
Code: platform.EEmptyValue,
Msg: "username is empty",
}
}
if req.Org == "" {
return nil, &platform.Error{
Code: platform.EEmptyValue,
Msg: "org name is empty",
}
}
if req.Bucket == "" {
return nil, &platform.Error{
Code: platform.EEmptyValue,
Msg: "bucket name is empty",
}
}
u := &platform.User{Name: req.User}
if err := c.CreateUser(ctx, u); err != nil {
return nil, err
}
if err = c.SetPassword(ctx, u.Name, req.Password); err != nil {
return nil, err
}
o := &platform.Organization{
Name: req.Org,
}
if err = c.CreateOrganization(ctx, o); err != nil {
return nil, err
}
bucket := &platform.Bucket{
Name: req.Bucket,
Organization: o.Name,
OrganizationID: o.ID,
}
if err = c.CreateBucket(ctx, bucket); err != nil {
return nil, err
}
auth := &platform.Authorization{
User: u.Name,
UserID: u.ID,
Permissions: []platform.Permission{
platform.CreateUserPermission,
platform.DeleteUserPermission,
platform.Permission{
Resource: platform.OrganizationResource,
Action: platform.WriteAction,
},
platform.WriteBucketPermission(bucket.ID),
},
}
if err = c.CreateAuthorization(ctx, auth); err != nil {
return nil, err
}
if err = c.PutOnboardingStatus(ctx, true); err != nil {
return nil, err
}
return &platform.OnboardingResults{
User: u,
Org: o,
Bucket: bucket,
Auth: auth,
}, nil
}

32
bolt/onboarding_test.go Normal file
View File

@ -0,0 +1,32 @@
package bolt_test
import (
"context"
"testing"
platformtesting "github.com/influxdata/platform/testing"
)
func initOnboardingService(f platformtesting.OnboardingFields, t *testing.T) (platformtesting.OnBoardingNBasicAuthService, func()) {
c, closeFn, err := NewTestClient()
if err != nil {
t.Fatalf("failed to create new bolt client: %v", err)
}
c.IDGenerator = f.IDGenerator
c.TokenGenerator = f.TokenGenerator
ctx := context.TODO()
if err = c.PutOnboardingStatus(ctx, !f.IsOnboarding); err != nil {
t.Fatalf("failed to set new onboarding finished: %v", err)
}
return c, func() {
defer closeFn()
if err := c.PutOnboardingStatus(ctx, false); err != nil {
t.Logf("failed to remove onboarding finished: %v", err)
}
}
}
func TestGenerate(t *testing.T) {
platformtesting.Generate(initOnboardingService, t)
}

View File

@ -184,6 +184,8 @@ func platformF(cmd *cobra.Command, args []string) {
macroSvc = c
}
var onboardingSvc platform.OnboardingService = c
var queryService query.QueryService
{
// TODO(lh): this is temporary until query endpoint is added here.
@ -303,6 +305,9 @@ func platformF(cmd *cobra.Command, args []string) {
sourceHandler.NewBucketService = source.NewBucketService
sourceHandler.NewQueryService = source.NewQueryService
setupHandler := http.NewSetupHandler()
setupHandler.OnboardingService = onboardingSvc
taskHandler := http.NewTaskHandler(logger)
taskHandler.TaskService = taskSvc
@ -340,6 +345,7 @@ func platformF(cmd *cobra.Command, args []string) {
MacroHandler: macroHandler,
QueryHandler: queryHandler,
WriteHandler: writeHandler,
SetupHandler: setupHandler,
}
reg.MustRegister(platformHandler.PrometheusCollectors()...)

View File

@ -54,7 +54,7 @@ type Error struct {
}
// Error implement the error interface by outputing the Code and Err.
func (e Error) Error() string {
func (e *Error) Error() string {
var b strings.Builder
// Print the current operation in our stack, if any.

85
http/onboarding.go Normal file
View File

@ -0,0 +1,85 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/influxdata/platform"
"github.com/julienschmidt/httprouter"
)
// SetupHandler represents an HTTP API handler for onboarding setup.
type SetupHandler struct {
*httprouter.Router
OnboardingService platform.OnboardingService
}
// NewSetupHandler returns a new instance of SetupHandler.
func NewSetupHandler() *SetupHandler {
h := &SetupHandler{
Router: httprouter.New(),
}
h.HandlerFunc("POST", "/setup", h.handlePostSetup)
h.HandlerFunc("GET", "/setup", h.isOnboarding)
return h
}
// isOnboarding is the HTTP handler for the GET /setup route.
// returns true/false
func (h *SetupHandler) isOnboarding(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := h.OnboardingService.IsOnboarding(ctx)
if err != nil {
EncodeError(ctx, err, w)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"allowed": %v}`, result)
}
func (h *SetupHandler) handlePostSetup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostSetupRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
results, err := h.OnboardingService.Generate(ctx, req)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, newOnboardingResponse(results)); err != nil {
EncodeError(ctx, err, w)
return
}
}
type onboardingResponse struct {
User *userResponse `json:"user"`
Bucket *bucketResponse `json:"bucket"`
Organization *orgResponse `json:"org"`
Auth *authResponse `json:"auth"`
}
func newOnboardingResponse(results *platform.OnboardingResults) *onboardingResponse {
return &onboardingResponse{
User: newUserResponse(results.User),
Bucket: newBucketResponse(results.Bucket),
Organization: newOrgResponse(results.Org),
Auth: newAuthResponse(results.Auth),
}
}
func decodePostSetupRequest(ctx context.Context, r *http.Request) (*platform.OnboardingRequest, error) {
req := &platform.OnboardingRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, err
}
return req, nil
}

View File

@ -25,6 +25,7 @@ type PlatformHandler struct {
FluxLangHandler *FluxLangHandler
QueryHandler *FluxHandler
WriteHandler *WriteHandler
SetupHandler *SetupHandler
}
func setCORSResponseHeaders(w nethttp.ResponseWriter, r *nethttp.Request) {
@ -36,6 +37,7 @@ func setCORSResponseHeaders(w nethttp.ResponseWriter, r *nethttp.Request) {
}
var platformLinks = map[string]interface{}{
"setup": "/setup",
"sources": "/v2/sources",
"dashboards": "/v2/dashboards",
"query": "/v2/query",
@ -79,7 +81,8 @@ func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request
// of the platform API.
if !strings.HasPrefix(r.URL.Path, "/v1") &&
!strings.HasPrefix(r.URL.Path, "/v2") &&
!strings.HasPrefix(r.URL.Path, "/chronograf/") {
!strings.HasPrefix(r.URL.Path, "/chronograf/") &&
!strings.HasPrefix(r.URL.Path, "/setup") {
h.AssetHandler.ServeHTTP(w, r)
return
}
@ -90,6 +93,11 @@ func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request
return
}
if strings.HasPrefix(r.URL.Path, "/setup") {
h.SetupHandler.ServeHTTP(w, r)
return
}
ctx := r.Context()
var err error
if ctx, err = extractAuthorization(ctx, r); err != nil {

View File

@ -15,6 +15,38 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Routes"
/setup:
get:
tags:
- Setup
summary: check if database has default user, org, bucket created, returns true if not.
response:
'200':
content:
application/json:
type: object
properties:
allowed:
type: boolean
post:
tags:
- Setup
summary: post onboarding request, to setup initial user, org and bucket
requestBody:
description: source to create
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/OnboardingRequest"
responses:
'201':
description: Created default user, bucket, org
content:
application/json:
schema:
$ref: "#/components/schemas/OnboardingResponse"
/macros:
get:
tags:
@ -3230,6 +3262,28 @@ components:
type: array
items:
$ref: "#/components/schemas/Source"
OnboardingRequest:
type: object
properties:
username:
type: string
password:
type: string
org:
type: string
bucket:
type: string
OnboardingResponse:
type: object
properties:
user:
$ref: "#/components/schemas/User"
org:
$ref: "#/components/schemas/Organization"
bucket:
$ref: "#/components/schemas/Bucket"
auth:
$ref: "#/components/schemas/Authorization"
Health:
type: object
properties:

28
onboarding.go Normal file
View File

@ -0,0 +1,28 @@
package platform
import "context"
// OnboardingResults is a group of elements required for first run.
type OnboardingResults struct {
User *User `json:"user"`
Org *Organization `json:"org"`
Bucket *Bucket `json:"bucket"`
Auth *Authorization `json:"auth"`
}
// OnboardingRequest is the request
// to setup defaults.
type OnboardingRequest struct {
User string `json:"username"`
Password string `json:"password"`
Org string `json:"org"`
Bucket string `json:"bucket"`
}
// OnboardingService represents a service for the first run.
type OnboardingService interface {
// IsOnboarding determine if onboarding request is allowed.
IsOnboarding(ctx context.Context) (bool, error)
// Generate OnboardingResults.
Generate(ctx context.Context, req *OnboardingRequest) (*OnboardingResults, error)
}

244
testing/onboarding.go Normal file
View File

@ -0,0 +1,244 @@
package testing
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/platform"
"github.com/influxdata/platform/mock"
)
// OnboardingFields will include the IDGenerator, TokenGenerator
// and IsOnboarding
type OnboardingFields struct {
IDGenerator platform.IDGenerator
TokenGenerator platform.TokenGenerator
IsOnboarding bool
}
// OnBoardingNBasicAuthService includes onboarding service
// and basic auth service.
type OnBoardingNBasicAuthService interface {
platform.OnboardingService
platform.BasicAuthService
}
// Generate testing
func Generate(
init func(OnboardingFields, *testing.T) (OnBoardingNBasicAuthService, func()),
t *testing.T,
) {
type args struct {
request *platform.OnboardingRequest
}
type wants struct {
errCode string
results *platform.OnboardingResults
password string
}
tests := []struct {
name string
fields OnboardingFields
args args
wants wants
}{
{
name: "denied",
fields: OnboardingFields{
IDGenerator: &loopIDGenerator{
s: []string{oneID, twoID, threeID, fourID},
t: t,
},
TokenGenerator: mock.NewTokenGenerator(oneToken, nil),
IsOnboarding: false,
},
wants: wants{
errCode: platform.EConflict,
},
},
{
name: "missing password",
fields: OnboardingFields{
IDGenerator: &loopIDGenerator{
s: []string{oneID, twoID, threeID, fourID},
t: t,
},
TokenGenerator: mock.NewTokenGenerator(oneToken, nil),
IsOnboarding: true,
},
args: args{
request: &platform.OnboardingRequest{
User: "admin",
Org: "org1",
Bucket: "bucket1",
},
},
wants: wants{
errCode: platform.EEmptyValue,
},
},
{
name: "missing username",
fields: OnboardingFields{
IDGenerator: &loopIDGenerator{
s: []string{oneID, twoID, threeID, fourID},
t: t,
},
TokenGenerator: mock.NewTokenGenerator(oneToken, nil),
IsOnboarding: true,
},
args: args{
request: &platform.OnboardingRequest{
Org: "org1",
Bucket: "bucket1",
},
},
wants: wants{
errCode: platform.EEmptyValue,
},
},
{
name: "missing org",
fields: OnboardingFields{
IDGenerator: &loopIDGenerator{
s: []string{oneID, twoID, threeID, fourID},
t: t,
},
TokenGenerator: mock.NewTokenGenerator(oneToken, nil),
IsOnboarding: true,
},
args: args{
request: &platform.OnboardingRequest{
User: "admin",
Bucket: "bucket1",
},
},
wants: wants{
errCode: platform.EEmptyValue,
},
},
{
name: "missing bucket",
fields: OnboardingFields{
IDGenerator: &loopIDGenerator{
s: []string{oneID, twoID, threeID, fourID},
t: t,
},
TokenGenerator: mock.NewTokenGenerator(oneToken, nil),
IsOnboarding: true,
},
args: args{
request: &platform.OnboardingRequest{
User: "admin",
Org: "org1",
},
},
wants: wants{
errCode: platform.EEmptyValue,
},
},
{
name: "regular",
fields: OnboardingFields{
IDGenerator: &loopIDGenerator{
s: []string{oneID, twoID, threeID, fourID},
t: t,
},
TokenGenerator: mock.NewTokenGenerator(oneToken, nil),
IsOnboarding: true,
},
args: args{
request: &platform.OnboardingRequest{
User: "admin",
Org: "org1",
Bucket: "bucket1",
Password: "pass1",
},
},
wants: wants{
password: "pass1",
results: &platform.OnboardingResults{
User: &platform.User{
ID: idFromString(t, oneID),
Name: "admin",
},
Org: &platform.Organization{
ID: idFromString(t, twoID),
Name: "org1",
},
Bucket: &platform.Bucket{
ID: idFromString(t, threeID),
Name: "bucket1",
Organization: "org1",
OrganizationID: idFromString(t, twoID),
},
Auth: &platform.Authorization{
ID: idFromString(t, fourID),
Token: oneToken,
Status: platform.Active,
User: "admin",
UserID: idFromString(t, oneID),
Permissions: []platform.Permission{
platform.CreateUserPermission,
platform.DeleteUserPermission,
platform.Permission{
Resource: platform.OrganizationResource,
Action: platform.WriteAction,
},
platform.WriteBucketPermission(idFromString(t, threeID)),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
results, err := s.Generate(ctx, tt.args.request)
if (err != nil) != (tt.wants.errCode != "") {
t.Fatalf("expected error code '%s' got '%v'", tt.wants.errCode, err)
}
if err != nil && tt.wants.errCode != "" {
if code := platform.ErrorCode(err); code != tt.wants.errCode {
t.Fatalf("expected error code to match '%s' got '%v'", tt.wants.errCode, code)
}
}
if diff := cmp.Diff(results, tt.wants.results); diff != "" {
t.Errorf("onboarding results are different -got/+want\ndiff %s", diff)
}
if results != nil {
if err = s.ComparePassword(ctx, results.User.Name, tt.wants.password); err != nil {
t.Errorf("onboarding set password is wrong")
}
}
})
}
}
const (
oneID = "020f755c3c082000"
twoID = "020f755c3c082001"
threeID = "020f755c3c082002"
fourID = "020f755c3c082003"
oneToken = "020f755c3c082008"
)
type loopIDGenerator struct {
s []string
p int
t *testing.T
}
func (g *loopIDGenerator) ID() platform.ID {
if g.p == len(g.s) {
g.p = 0
}
id := idFromString(g.t, g.s[g.p])
g.p++
return id
}