feat(pkger): add stack init cmd to influx cli

closes: #17235
pull/17465/head
Johnny Steenbergen 2020-03-26 13:23:14 -07:00 committed by Johnny Steenbergen
parent 1a66ca3900
commit 37646464b3
11 changed files with 269 additions and 113 deletions

View File

@ -8,6 +8,7 @@
1. [17363](https://github.com/influxdata/influxdb/pull/17363): Telegraf config tokens can no longer be retrieved after creation, but new tokens can be created after a telegraf has been setup
1. [17400](https://github.com/influxdata/influxdb/pull/17400): Be able to delete bucket by name via cli
1. [17396](https://github.com/influxdata/influxdb/pull/17396): Add module to write line data to specified url, org, and bucket
1. [17448](https://github.com/influxdata/influxdb/pull/17448): Add foundation for pkger stacks, stateful package management
### Bug Fixes

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -88,6 +89,10 @@ func (o genericCLIOpts) newCmd(use string, runE func(*cobra.Command, []string) e
return cmd
}
func (o genericCLIOpts) writeJSON(v interface{}) error {
return json.NewEncoder(o.w).Encode(v)
}
func (o genericCLIOpts) newTabWriter() *internal.TabWriter {
return internal.NewTabWriter(o.w)
}

View File

@ -42,8 +42,11 @@ type cmdPkgBuilder struct {
file string
files []string
filters []string
description string
disableColor bool
disableTableBorders bool
json bool
name string
org organization
quiet bool
recurse bool
@ -82,7 +85,9 @@ func (b *cmdPkgBuilder) cmd() *cobra.Command {
b.cmdPkgExport(),
b.cmdPkgSummary(),
b.cmdPkgValidate(),
b.cmdStack(),
)
return cmd
}
@ -345,6 +350,68 @@ func (b *cmdPkgBuilder) cmdPkgValidate() *cobra.Command {
return cmd
}
func (b *cmdPkgBuilder) cmdStack() *cobra.Command {
cmd := b.newCmd("stack", nil, false)
cmd.Short = "Stack management commands"
cmd.AddCommand(b.cmdStackInit())
return cmd
}
func (b *cmdPkgBuilder) cmdStackInit() *cobra.Command {
cmd := b.newCmd("init", b.stackInitRunEFn, true)
cmd.Short = "Initialize a stack"
cmd.Flags().StringVarP(&b.name, "stack-name", "n", "", "Name given to created stack")
cmd.Flags().StringVarP(&b.description, "stack-description", "d", "", "Description given to created stack")
cmd.Flags().StringArrayVarP(&b.urls, "package-url", "u", nil, "Package urls to associate with new stack")
cmd.Flags().BoolVar(&b.json, "json", false, "Output data as json")
b.org.register(cmd, false)
return cmd
}
func (b *cmdPkgBuilder) stackInitRunEFn(cmd *cobra.Command, args []string) error {
pkgSVC, orgSVC, err := b.svcFn()
if err != nil {
return err
}
orgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
const fakeUserID = 0 // is 0 because user is pulled from token...
stack, err := pkgSVC.InitStack(context.Background(), fakeUserID, pkger.Stack{
OrgID: orgID,
Name: b.name,
Description: b.description,
URLs: b.urls,
})
if err != nil {
return err
}
if b.json {
return b.writeJSON(stack)
}
tabW := b.newTabWriter()
tabW.WriteHeaders("ID", "OrgID", "Name", "Description", "URLs", "Created At")
tabW.Write(map[string]interface{}{
"ID": stack.ID,
"OrgID": stack.OrgID,
"Name": stack.Name,
"Description": stack.Description,
"URLs": stack.URLs,
"Created At": stack.CreatedAt,
})
tabW.Flush()
return nil
}
func (b *cmdPkgBuilder) registerPkgFileFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVarP(&b.files, "file", "f", nil, "Path to package file")
cmd.MarkFlagFilename("file", "yaml", "yml", "json", "jsonnet")

View File

@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"io/ioutil"
@ -447,6 +448,122 @@ func TestCmdPkg(t *testing.T) {
require.Error(t, cmd.Execute())
})
})
t.Run("stack", func(t *testing.T) {
t.Run("init", func(t *testing.T) {
tests := []struct {
name string
args []string
envVars map[string]string
expectedStack pkger.Stack
shouldErr bool
}{
{
name: "when only org and token provided is successful",
args: []string{"--org-id=" + influxdb.ID(1).String()},
expectedStack: pkger.Stack{
OrgID: 1,
},
},
{
name: "when org and name provided provided is successful",
args: []string{
"--org-id=" + influxdb.ID(1).String(),
"--stack-name=foo",
},
expectedStack: pkger.Stack{
OrgID: 1,
Name: "foo",
},
},
{
name: "when all flags provided provided is successful",
args: []string{
"--org-id=" + influxdb.ID(1).String(),
"--stack-name=foo",
"--stack-description=desc",
"--package-url=http://example.com/1",
"--package-url=http://example.com/2",
},
expectedStack: pkger.Stack{
OrgID: 1,
Name: "foo",
Description: "desc",
URLs: []string{
"http://example.com/1",
"http://example.com/2",
},
},
},
{
name: "when all shorthand flags provided provided is successful",
args: []string{
"--org-id=" + influxdb.ID(1).String(),
"-n=foo",
"-d=desc",
"-u=http://example.com/1",
"-u=http://example.com/2",
},
expectedStack: pkger.Stack{
OrgID: 1,
Name: "foo",
Description: "desc",
URLs: []string{
"http://example.com/1",
"http://example.com/2",
},
},
},
}
for _, tt := range tests {
fn := func(t *testing.T) {
defer addEnvVars(t, envVarsZeroMap)()
outBuf := new(bytes.Buffer)
defer func() {
if t.Failed() && outBuf.Len() > 0 {
t.Log(outBuf.String())
}
}()
builder := newInfluxCmdBuilder(
in(new(bytes.Buffer)),
out(outBuf),
)
rootCmd := builder.cmd(func(f *globalFlags, opt genericCLIOpts) *cobra.Command {
echoSVC := &fakePkgSVC{
initStackFn: func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
stack.ID = 9000
return stack, nil
},
}
return newCmdPkgBuilder(fakeSVCFn(echoSVC), opt).cmd()
})
baseArgs := []string{"pkg", "stack", "init", "--json"}
rootCmd.SetArgs(append(baseArgs, tt.args...))
err := rootCmd.Execute()
if tt.shouldErr {
require.Error(t, err)
} else {
require.NoError(t, err)
var stack pkger.Stack
testDecodeJSONBody(t, outBuf, &stack)
if tt.expectedStack.ID == 0 {
tt.expectedStack.ID = 9000
}
assert.Equal(t, tt.expectedStack, stack)
}
}
t.Run(tt.name, fn)
}
})
})
}
func Test_readFilesFromPath(t *testing.T) {
@ -586,12 +703,16 @@ func testPkgWritesToBuffer(newCmdFn func(w io.Writer) *cobra.Command, args pkgFi
}
type fakePkgSVC struct {
createFn func(ctx context.Context, setters ...pkger.CreatePkgSetFn) (*pkger.Pkg, error)
dryRunFn func(ctx context.Context, orgID, userID influxdb.ID, pkg *pkger.Pkg) (pkger.Summary, pkger.Diff, error)
applyFn func(ctx context.Context, orgID, userID influxdb.ID, pkg *pkger.Pkg, opts ...pkger.ApplyOptFn) (pkger.Summary, error)
initStackFn func(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error)
createFn func(ctx context.Context, setters ...pkger.CreatePkgSetFn) (*pkger.Pkg, error)
dryRunFn func(ctx context.Context, orgID, userID influxdb.ID, pkg *pkger.Pkg) (pkger.Summary, pkger.Diff, error)
applyFn func(ctx context.Context, orgID, userID influxdb.ID, pkg *pkger.Pkg, opts ...pkger.ApplyOptFn) (pkger.Summary, error)
}
func (f *fakePkgSVC) InitStack(ctx context.Context, userID influxdb.ID, stack pkger.Stack) (pkger.Stack, error) {
if f.initStackFn != nil {
return f.initStackFn(ctx, userID, stack)
}
panic("not implemented")
}
@ -639,3 +760,10 @@ func idsStr(ids ...influxdb.ID) string {
}
return strings.Join(idStrs, ",")
}
func testDecodeJSONBody(t *testing.T, r io.Reader, v interface{}) {
t.Helper()
err := json.NewDecoder(r).Decode(v)
require.NoError(t, err)
}

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"net/url"
"testing"
"time"
@ -25,22 +24,22 @@ func TestLauncher_Pkger(t *testing.T) {
svc := l.PkgerService(t)
t.Run("creating a stack", func(t *testing.T) {
expectedURLs := []url.URL{newURL(t, "http://example.com")}
expectedURLs := []string{"http://example.com"}
fmt.Println("org init id: ", l.Org.ID)
newStack, err := svc.InitStack(timedCtx(5*time.Second), l.User.ID, pkger.Stack{
OrgID: l.Org.ID,
Name: "first stack",
Desc: "desc",
URLs: expectedURLs,
OrgID: l.Org.ID,
Name: "first stack",
Description: "desc",
URLs: expectedURLs,
})
require.NoError(t, err)
assert.NotZero(t, newStack.ID)
assert.Equal(t, l.Org.ID, newStack.OrgID)
assert.Equal(t, "first stack", newStack.Name)
assert.Equal(t, "desc", newStack.Desc)
assert.Equal(t, "desc", newStack.Description)
assert.Equal(t, expectedURLs, newStack.URLs)
assert.NotNil(t, newStack.Resources)
assert.NotZero(t, newStack.CRUDLog)
@ -1290,11 +1289,3 @@ func (f *fakeLabelSVC) CreateLabelMapping(ctx context.Context, m *influxdb.Label
}
return f.LabelService.CreateLabelMapping(ctx, m)
}
func newURL(t *testing.T, rawurl string) url.URL {
t.Helper()
u, err := url.Parse(rawurl)
require.NoError(t, err)
return *u
}

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/pkg/httpc"
@ -21,10 +20,8 @@ func (s *HTTPRemoteService) InitStack(ctx context.Context, userID influxdb.ID, s
reqBody := ReqCreateStack{
OrgID: stack.OrgID.String(),
Name: stack.Name,
Description: stack.Desc,
}
for _, u := range stack.URLs {
reqBody.URLs = append(reqBody.URLs, u.String())
Description: stack.Description,
URLs: stack.URLs,
}
var respBody RespCreateStack
@ -37,10 +34,11 @@ func (s *HTTPRemoteService) InitStack(ctx context.Context, userID influxdb.ID, s
}
newStack := Stack{
Name: respBody.Name,
Desc: respBody.Description,
Resources: make([]StackResource, 0),
CRUDLog: respBody.CRUDLog,
Name: respBody.Name,
Description: respBody.Description,
URLs: respBody.URLs,
Resources: make([]StackResource, 0),
CRUDLog: respBody.CRUDLog,
}
id, err := influxdb.IDFromString(respBody.ID)
@ -57,14 +55,6 @@ func (s *HTTPRemoteService) InitStack(ctx context.Context, userID influxdb.ID, s
}
newStack.OrgID = *orgID
for _, rawurl := range respBody.URLs {
u, err := url.Parse(rawurl)
if err != nil {
return Stack{}, err
}
newStack.URLs = append(newStack.URLs, *u)
}
return newStack, nil
}

View File

@ -96,15 +96,6 @@ func (r *ReqCreateStack) orgID() influxdb.ID {
return *orgID
}
func (r *ReqCreateStack) urls() []url.URL {
urls := make([]url.URL, 0, len(r.URLs))
for _, urlStr := range r.URLs {
u, _ := url.Parse(urlStr)
urls = append(urls, *u)
}
return urls
}
// RespCreateStack is the response body for the create stack call.
type RespCreateStack struct {
ID string `json:"id"`
@ -130,27 +121,22 @@ func (s *HTTPServer) createStack(w http.ResponseWriter, r *http.Request) {
}
stack, err := s.svc.InitStack(r.Context(), auth.GetUserID(), Stack{
OrgID: reqBody.orgID(),
Name: reqBody.Name,
Desc: reqBody.Description,
URLs: reqBody.urls(),
OrgID: reqBody.orgID(),
Name: reqBody.Name,
Description: reqBody.Description,
URLs: reqBody.URLs,
})
if err != nil {
s.api.Err(w, err)
return
}
urlStrs := make([]string, 0, len(stack.URLs))
for _, u := range stack.URLs {
urlStrs = append(urlStrs, u.String())
}
s.api.Respond(w, http.StatusCreated, RespCreateStack{
ID: stack.ID.String(),
OrgID: stack.OrgID.String(),
Name: stack.Name,
Description: stack.Desc,
URLs: urlStrs,
Description: stack.Description,
URLs: stack.URLs,
CRUDLog: stack.CRUDLog,
})
}

View File

@ -25,12 +25,12 @@ type (
// platform. This stack is updated only after side effects of applying a pkg.
// If the pkg is applied, and no changes are had, then the stack is not updated.
Stack struct {
ID influxdb.ID
OrgID influxdb.ID
Name string
Desc string
URLs []url.URL
Resources []StackResource
ID influxdb.ID `json:"id"`
OrgID influxdb.ID `json:"orgID"`
Name string `json:"name"`
Description string `json:"description"`
URLs []string `json:"urls"`
Resources []StackResource `json:"resources"`
influxdb.CRUDLog
}
@ -38,10 +38,10 @@ type (
// StackResource is a record for an individual resource side effect genereated from
// applying a pkg.
StackResource struct {
APIVersion string
ID influxdb.ID
Kind Kind
Name string
APIVersion string `json:"apiVersion"`
ID influxdb.ID `json:"resourceID"`
Kind Kind `json:"kind"`
Name string `json:"pkgName"`
}
)
@ -260,6 +260,10 @@ func NewService(opts ...ServiceSetterFn) *Service {
// with urls that point to the location of packages that are included as part of the stack when
// it is applied.
func (s *Service) InitStack(ctx context.Context, userID influxdb.ID, stack Stack) (Stack, error) {
if err := validURLs(stack.URLs); err != nil {
return Stack{}, err
}
if _, err := s.orgSVC.FindOrganizationByID(ctx, stack.OrgID); err != nil {
if influxdb.ErrorCode(err) == influxdb.ENotFound {
msg := fmt.Sprintf("organization dependency does not exist for id[%q]", stack.OrgID.String())
@ -2253,6 +2257,16 @@ func (a applyErrs) toError(resType, msg string) error {
return errors.New(errMsg)
}
func validURLs(urls []string) error {
for _, u := range urls {
if _, err := url.Parse(u); err != nil {
msg := fmt.Sprintf("url invalid for entry %q", u)
return toInfluxError(influxdb.EInvalid, msg)
}
}
return nil
}
func labelSlcToMap(labels []*label) map[string]*label {
m := make(map[string]*label)
for i := range labels {

View File

@ -27,21 +27,18 @@ var _ SVC = (*loggingMW)(nil)
func (s *loggingMW) InitStack(ctx context.Context, userID influxdb.ID, newStack Stack) (stack Stack, err error) {
defer func(start time.Time) {
if err != nil {
urlStrs := make([]string, 0, len(newStack.URLs))
for _, u := range newStack.URLs {
urlStrs = append(urlStrs, u.String())
}
s.logger.Error(
"failed to init stack",
zap.Error(err),
zap.Duration("took", time.Since(start)),
zap.Stringer("orgID", newStack.OrgID),
zap.Stringer("userID", userID),
zap.Strings("urls", urlStrs),
)
if err == nil {
return
}
s.logger.Error(
"failed to init stack",
zap.Error(err),
zap.Duration("took", time.Since(start)),
zap.Stringer("orgID", newStack.OrgID),
zap.Stringer("userID", userID),
zap.Strings("urls", newStack.URLs),
)
}(time.Now())
return s.next.InitStack(ctx, userID, newStack)
}

View File

@ -3,7 +3,6 @@ package pkger
import (
"context"
"encoding/json"
"net/url"
"time"
"github.com/influxdata/influxdb"
@ -175,19 +174,14 @@ func convertStackToEnt(stack Stack) (kv.Entity, error) {
return kv.Entity{}, err
}
urlStrs := make([]string, 0, len(stack.URLs))
for _, u := range stack.URLs {
urlStrs = append(urlStrs, u.String())
}
stEnt := entStack{
ID: idBytes,
OrgID: orgIDBytes,
Name: stack.Name,
Description: stack.Desc,
Description: stack.Description,
CreatedAt: stack.CreatedAt,
UpdatedAt: stack.UpdatedAt,
URLs: urlStrs,
URLs: stack.URLs,
}
for _, res := range stack.Resources {
@ -208,8 +202,9 @@ func convertStackToEnt(stack Stack) (kv.Entity, error) {
func convertStackEntToStack(ent *entStack) (Stack, error) {
stack := Stack{
Name: ent.Name,
Desc: ent.Description,
Name: ent.Name,
Description: ent.Description,
URLs: ent.URLs,
CRUDLog: influxdb.CRUDLog{
CreatedAt: ent.CreatedAt,
UpdatedAt: ent.UpdatedAt,
@ -223,14 +218,6 @@ func convertStackEntToStack(ent *entStack) (Stack, error) {
return Stack{}, err
}
for _, urlStr := range ent.URLs {
u, err := url.Parse(urlStr)
if err != nil {
return Stack{}, err
}
stack.URLs = append(stack.URLs, *u)
}
for _, res := range ent.Resources {
stackRes := StackResource{
APIVersion: res.APIVersion,

View File

@ -2,7 +2,6 @@ package pkger_test
import (
"context"
"net/url"
"testing"
"time"
@ -19,17 +18,17 @@ func TestStoreKV(t *testing.T) {
stackStub := func(id, orgID influxdb.ID) pkger.Stack {
now := time.Time{}.Add(10 * 365 * 24 * time.Hour)
return pkger.Stack{
ID: id,
OrgID: orgID,
Name: "threeve",
Desc: "desc",
ID: id,
OrgID: orgID,
Name: "threeve",
Description: "desc",
CRUDLog: influxdb.CRUDLog{
CreatedAt: now,
UpdatedAt: now.Add(time.Hour),
},
URLs: []url.URL{
newURL(t, "http://example.com"),
newURL(t, "http://abc.gov"),
URLs: []string{
"http://example.com",
"http://abc.gov",
},
Resources: []pkger.StackResource{
{
@ -197,12 +196,3 @@ func seedEntities(t *testing.T, store pkger.Store, first pkger.Stack, rest ...pk
require.NoError(t, err)
}
}
func newURL(t *testing.T, rawurl string) url.URL {
t.Helper()
u, err := url.Parse(rawurl)
require.NoError(t, err)
return *u
}