diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 39852c8..fe8fb18 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -49,7 +49,7 @@ jobs: # working-directory: somedir # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 + args: --timeout=3m # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true diff --git a/cmd/fl/app/app.go b/cmd/fl/app/app.go index f98f11e..7128a7b 100644 --- a/cmd/fl/app/app.go +++ b/cmd/fl/app/app.go @@ -21,6 +21,7 @@ import ( "github.com/alecthomas/kong" "github.com/funlessdev/fl-cli/internal/command/admin" + "github.com/funlessdev/fl-cli/internal/command/cfg" "github.com/funlessdev/fl-cli/internal/command/fn" "github.com/funlessdev/fl-cli/internal/command/mod" "github.com/funlessdev/fl-cli/internal/command/template" @@ -36,6 +37,7 @@ type CLI struct { Mod mod.Mod `cmd:"" help:"Create, delete and manage modules"` Admin admin.Admin `cmd:"" aliases:"a" help:"Deploy and manage the platform"` Template template.Template `cmd:"" help:"Pull function templates"` + Cfg cfg.Cfg `cmd:"" aliases:"c,config" help:"Manage local configuration"` Version kong.VersionFlag `short:"v" cmd:"" passthrough:"" help:"Show fl version"` } @@ -56,7 +58,10 @@ func ParseCMD(version string) (*kong.Context, error) { return nil, err } - flConfig := client.Config{Host: "http://localhost:4000"} + flConfig, err := client.NewConfig(pkg.ConfigFileName) + if err != nil { + return nil, err + } flClient, err := client.NewClient(http.DefaultClient, flConfig) validator := client.InputValidator{} if err != nil { @@ -78,14 +83,16 @@ func ParseCMD(version string) (*kong.Context, error) { kong.BindTo(ctx, (*context.Context)(nil)), kong.BindTo(fnSvc, (*client.FnHandler)(nil)), kong.BindTo(modSvc, (*client.ModHandler)(nil)), + kong.BindTo(userSvc, (*client.UserHandler)(nil)), kong.BindTo(logger, (*log.FLogger)(nil)), kong.BindTo(dockerShell, (*deploy.DockerShell)(nil)), kong.BindTo(kubernetesDeployer, (*deploy.KubernetesDeployer)(nil)), kong.BindTo(kubernetesRemover, (*deploy.KubernetesRemover)(nil)), kong.BindTo(wasmBuilder, (*build.DockerBuilder)(nil)), - kong.BindTo(userSvc, (*client.UserHandler)(nil)), + kong.BindTo(flConfig, (*client.Config)(nil)), kong.Vars{ "version": version, + "config_keys": pkg.ConfigKeys, "default_core_image": pkg.CoreImg, "default_worker_image": pkg.WorkerImg, }, diff --git a/go.mod b/go.mod index 78d9e04..2621b63 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/funlessdev/fl-client-sdk-go v0.0.0-20230312081443-2c80f8dc5ba5 github.com/theckman/yacspin v0.13.12 golang.org/x/exp v0.0.0-20230223210539-50820d90acfd + gopkg.in/yaml.v2 v2.4.0 gotest.tools/v3 v3.4.0 k8s.io/api v0.26.1 k8s.io/apimachinery v0.26.1 @@ -51,7 +52,7 @@ require ( github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/imdario/mergo v0.3.13 // indirect + github.com/imdario/mergo v0.3.14 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -60,6 +61,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -89,7 +91,6 @@ require ( google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.90.0 // indirect k8s.io/kube-openapi v0.0.0-20230127205639-68031ae9242a // indirect @@ -103,5 +104,5 @@ require ( github.com/go-git/go-git/v5 v5.5.2 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 // indirect - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 ) diff --git a/go.sum b/go.sum index 021f865..7f1aee0 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -59,10 +60,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/funlessdev/fl-client-sdk-go v0.0.0-20230126150929-357b8efdaab0 h1:sR6tDlHDrfvheLt8cDQddgE5nUX+mD5d2nmAEiFZD98= -github.com/funlessdev/fl-client-sdk-go v0.0.0-20230126150929-357b8efdaab0/go.mod h1:ysqKhDMya3Qzmu37XEpkbGuNWk67I88e93g47GT5vz4= -github.com/funlessdev/fl-client-sdk-go v0.0.0-20230218195230-0cef4bf202e2 h1:8GtBi2j3ApEv8ffiasZLsVKVwyGKhF3PqC9Ip7DGeTA= -github.com/funlessdev/fl-client-sdk-go v0.0.0-20230218195230-0cef4bf202e2/go.mod h1:hnAtdQhg4An6FDhte2RWdM5C6v2kZ78+f/4/dN/+cGU= github.com/funlessdev/fl-client-sdk-go v0.0.0-20230312081443-2c80f8dc5ba5 h1:8Ax6MDbmmXOM/+McMj2iR3DC/BX6XnQpWPX7GfE0Gy4= github.com/funlessdev/fl-client-sdk-go v0.0.0-20230312081443-2c80f8dc5ba5/go.mod h1:hnAtdQhg4An6FDhte2RWdM5C6v2kZ78+f/4/dN/+cGU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -119,10 +116,12 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo= +github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= @@ -155,6 +154,8 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -209,8 +210,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= diff --git a/internal/command/admin/deploy/docker/docker_down.go b/internal/command/admin/deploy/docker/docker_down.go index 9cbf2b8..1b2c7bc 100644 --- a/internal/command/admin/deploy/docker/docker_down.go +++ b/internal/command/admin/deploy/docker/docker_down.go @@ -34,7 +34,7 @@ func (r *Down) Run(ctx context.Context, dk deploy.DockerShell, logger log.FLogge if err != nil { errorMsg := "unable to read docker-compose.yml file" if os.IsNotExist(err) { - lines, _ := dk.ComposeList() + lines, _ := dk.ComposeList(ctx) if slices.Contains(lines, "fl") { errorMsg = "unable to locate docker-compose.yml, but a local deployment was found. The file might have been moved or deleted." } else { @@ -45,7 +45,7 @@ func (r *Down) Run(ctx context.Context, dk deploy.DockerShell, logger log.FLogge } defer os.Remove(composeFilePath) - err = dk.ComposeDown(composeFilePath) + err = dk.ComposeDown(ctx, composeFilePath) if err != nil { return err } diff --git a/internal/command/admin/deploy/docker/docker_down_test.go b/internal/command/admin/deploy/docker/docker_down_test.go index 167b878..e501fcc 100644 --- a/internal/command/admin/deploy/docker/docker_down_test.go +++ b/internal/command/admin/deploy/docker/docker_down_test.go @@ -22,6 +22,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/homedir" "github.com/funlessdev/fl-cli/test/mocks" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -60,7 +61,7 @@ func TestDockerDownRun(t *testing.T) { require.NoError(t, err) out.Reset() - mockDockerShell.On("ComposeDown", path).Return(errors.New("some compose down error")).Once() + mockDockerShell.On("ComposeDown", mock.Anything, path).Return(errors.New("some compose down error")).Once() err = down.Run(ctx, mockDockerShell, logger) require.Error(t, err) @@ -78,7 +79,7 @@ func TestDockerDownRun(t *testing.T) { }() out.Reset() - mockDockerShell.On("ComposeList").Return([]string{"fl"}, nil).Once() + mockDockerShell.On("ComposeList", mock.Anything).Return([]string{"fl"}, nil).Once() err = down.Run(ctx, mockDockerShell, logger) require.Error(t, err) @@ -98,7 +99,7 @@ func TestDockerDownRun(t *testing.T) { }() out.Reset() - mockDockerShell.On("ComposeList").Return([]string{"test"}, nil).Once() + mockDockerShell.On("ComposeList", mock.Anything).Return([]string{"test"}, nil).Once() err = down.Run(ctx, mockDockerShell, logger) require.Error(t, err) @@ -111,7 +112,7 @@ func TestDockerDownRun(t *testing.T) { require.NoError(t, err) out.Reset() - mockDockerShell.On("ComposeDown", path).Return(nil) + mockDockerShell.On("ComposeDown", mock.Anything, path).Return(nil) err = down.Run(ctx, mockDockerShell, logger) require.NoError(t, err) diff --git a/internal/command/admin/deploy/docker/docker_up.go b/internal/command/admin/deploy/docker/docker_up.go index 61b0ddc..343b1ca 100644 --- a/internal/command/admin/deploy/docker/docker_up.go +++ b/internal/command/admin/deploy/docker/docker_up.go @@ -19,12 +19,13 @@ import ( "errors" "io" "net/http" - "strings" "github.com/funlessdev/fl-cli/pkg" + "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/deploy" "github.com/funlessdev/fl-cli/pkg/homedir" "github.com/funlessdev/fl-cli/pkg/log" + "gopkg.in/yaml.v2" ) const ( @@ -52,9 +53,12 @@ EXAMPLES $ fl admin deploy docker up --core --worker ` } -func (u *Up) Run(ctx context.Context, dk deploy.DockerShell, logger log.FLogger) error { +func (u *Up) Run(ctx context.Context, dk deploy.DockerShell, logger log.FLogger, config client.Config) error { logger.Info("Deploying FunLess locally...\n\n") + cmdEnv := map[string]string{"SECRET_KEY_BASE": config.SecretKeyBase} + ctx = context.WithValue(ctx, pkg.FLContextKey("env"), cmdEnv) + _ = logger.StartSpinner("Setting things up...") composeFilePath, err := downloadFile("docker-compose.yml", dockerComposeYmlUrl) @@ -84,11 +88,20 @@ func (u *Up) Run(ctx context.Context, dk deploy.DockerShell, logger log.FLogger) _ = logger.StopSpinner(nil) - if err := dk.ComposeUp(composeFilePath); err != nil { + if err := dk.ComposeUp(ctx, composeFilePath); err != nil { return err } - logger.Info("\nDeployment complete!\n") + logger.Info("\nExtracting auth tokens... 🔒\n\n") + + err = dk.LogTokens(ctx) + if err != nil { + logger.Info("Couldn't extract auth tokens from core container. Completing deployment...") + } else { + logger.Info("\n\nRemember to add these tokens in ~/.fl/config as api_token and admin_token.") + } + + logger.Info("\n\nDeployment complete!\n") logger.Info("You can now start using FunLess! 🎉\n") return nil @@ -141,23 +154,35 @@ func downloadFolderFile(folder, file, url string) error { } func replaceImages(core string, worker string) error { - if core == pkg.CoreImg && worker == pkg.WorkerImg { - return nil - } - content, _, err := homedir.ReadFromConfigDir("docker-compose.yml") if err != nil { return errors.New("unable to read docker-compose.yml") } - newCompose := string(content) - if core != pkg.CoreImg { - newCompose = strings.Replace(string(content), pkg.CoreImg, core, 1) + var composeYaml map[string]interface{} + err = yaml.Unmarshal(content, &composeYaml) + if err != nil { + return err } - if worker != pkg.WorkerImg { - newCompose = strings.Replace(newCompose, pkg.WorkerImg, worker, 1) + + svc := composeYaml["services"].(map[interface{}]interface{}) + svcCore := svc["core"].(map[interface{}]interface{}) + svcWorker := svc["worker"].(map[interface{}]interface{}) + + svcCore["image"] = core + svcWorker["image"] = worker + + svc["core"] = svcCore + svc["worker"] = svcWorker + composeYaml["services"] = svc + + newCompose, err := yaml.Marshal(composeYaml) + + if err != nil { + return err } _, err = homedir.WriteToConfigDir("docker-compose.yml", []byte(newCompose), true) + return err } diff --git a/internal/command/admin/deploy/docker/docker_up_test.go b/internal/command/admin/deploy/docker/docker_up_test.go index 67af73c..56c7fca 100644 --- a/internal/command/admin/deploy/docker/docker_up_test.go +++ b/internal/command/admin/deploy/docker/docker_up_test.go @@ -23,11 +23,13 @@ import ( "testing" "github.com/funlessdev/fl-cli/pkg" + "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/homedir" "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" ) func TestDockerUpRun(t *testing.T) { @@ -49,7 +51,7 @@ func TestDockerUpRun(t *testing.T) { homedir.GetHomeDir = func() (string, error) { return "", errors.New("some home error") } - err := up.Run(ctx, mockDockerShell, logger) + err := up.Run(ctx, mockDockerShell, logger, client.Config{}) require.Error(t, err) }) @@ -57,8 +59,9 @@ func TestDockerUpRun(t *testing.T) { homedir.GetHomeDir = func() (string, error) { return homedirPath, nil } - mockDockerShell.On("ComposeUp", mock.Anything).Return(nil).Once() - err := up.Run(ctx, mockDockerShell, logger) + mockDockerShell.On("ComposeUp", mock.Anything, mock.Anything).Return(nil).Once() + mockDockerShell.On("LogTokens", mock.Anything).Return(nil).Once() + err := up.Run(ctx, mockDockerShell, logger, client.Config{}) require.NoError(t, err) require.Contains(t, out.String(), "\nDeployment complete!") @@ -66,21 +69,22 @@ func TestDockerUpRun(t *testing.T) { t.Run("should return error when compose up fails", func(t *testing.T) { out.Reset() - mockDockerShell.On("ComposeUp", mock.Anything).Return(errors.New("compose up error")).Once() - err := up.Run(ctx, mockDockerShell, logger) + mockDockerShell.On("ComposeUp", mock.Anything, mock.Anything).Return(errors.New("compose up error")).Once() + err := up.Run(ctx, mockDockerShell, logger, client.Config{}) require.Error(t, err) }) t.Run("should modify docker-compose.yml when given custom core/worker", func(t *testing.T) { out.Reset() - mockDockerShell.On("ComposeUp", mock.Anything).Return(nil).Once() + mockDockerShell.On("ComposeUp", mock.Anything, mock.Anything).Return(nil).Once() + mockDockerShell.On("LogTokens", mock.Anything).Return(nil).Once() _, path, err := homedir.ReadFromConfigDir("docker-compose.yml") require.NoError(t, err) os.Remove(path) up.CoreImage = "custom-core" up.WorkerImage = "custom-worker" - err = up.Run(ctx, mockDockerShell, logger) + err = up.Run(ctx, mockDockerShell, logger, client.Config{}) require.NoError(t, err) require.Contains(t, out.String(), "\nDeployment complete!") @@ -164,12 +168,21 @@ func Test_replaceImages(t *testing.T) { content, _, err := homedir.ReadFromConfigDir("docker-compose.yml") require.NoError(t, err) - expected := ` core: - image: core-test` - expectedWorker := ` worker: - image: ghcr.io/funlessdev/worker:latest` - require.Contains(t, string(content), expected, "core image should be the one provided") - require.Contains(t, string(content), expectedWorker, "worker image should be the default") + var contentYaml map[string]interface{} + err = yaml.Unmarshal(content, &contentYaml) + require.NoError(t, err) + + svc, ok := contentYaml["services"].(map[interface{}]interface{}) + require.True(t, ok) + svcCore, ok := svc["core"].(map[interface{}]interface{}) + require.True(t, ok) + svcWorker, ok := svc["worker"].(map[interface{}]interface{}) + require.True(t, ok) + + expected := "core-test" + expectedWorker := "ghcr.io/funlessdev/worker:latest" + require.Equal(t, svcCore["image"], expected, "core image should be the one provided") + require.Equal(t, svcWorker["image"], expectedWorker, "worker image should be the default") }) t.Run("should swap worker image when different from default", func(t *testing.T) { @@ -183,12 +196,21 @@ func Test_replaceImages(t *testing.T) { content, _, err := homedir.ReadFromConfigDir("docker-compose.yml") require.NoError(t, err) - expected := ` core: - image: ghcr.io/funlessdev/core:latest` - expectedWorker := ` worker: - image: worker-test` - require.Contains(t, string(content), expected, "core image should be the default") - require.Contains(t, string(content), expectedWorker, "worker image should be the one provided") + var contentYaml map[string]interface{} + err = yaml.Unmarshal(content, &contentYaml) + require.NoError(t, err) + + svc, ok := contentYaml["services"].(map[interface{}]interface{}) + require.True(t, ok) + svcCore, ok := svc["core"].(map[interface{}]interface{}) + require.True(t, ok) + svcWorker, ok := svc["worker"].(map[interface{}]interface{}) + require.True(t, ok) + + expected := "ghcr.io/funlessdev/core:latest" + expectedWorker := "worker-test" + require.Equal(t, svcCore["image"], expected, "core image should be the one provided") + require.Equal(t, svcWorker["image"], expectedWorker, "worker image should be the default") }) t.Run("should swap both images when different from default", func(t *testing.T) { @@ -202,12 +224,21 @@ func Test_replaceImages(t *testing.T) { content, _, err := homedir.ReadFromConfigDir("docker-compose.yml") require.NoError(t, err) - expected := ` core: - image: core-test` - expectedWorker := ` worker: - image: worker-test` - require.Contains(t, string(content), expected, "core image should be the one provided") - require.Contains(t, string(content), expectedWorker, "worker image should be the one provided") + var contentYaml map[string]interface{} + err = yaml.Unmarshal(content, &contentYaml) + require.NoError(t, err) + + svc, ok := contentYaml["services"].(map[interface{}]interface{}) + require.True(t, ok) + svcCore, ok := svc["core"].(map[interface{}]interface{}) + require.True(t, ok) + svcWorker, ok := svc["worker"].(map[interface{}]interface{}) + require.True(t, ok) + + expected := "core-test" + expectedWorker := "worker-test" + require.Equal(t, svcCore["image"], expected, "core image should be the one provided") + require.Equal(t, svcWorker["image"], expectedWorker, "worker image should be the default") }) } diff --git a/internal/command/admin/deploy/kubernetes/kubernetes_up.go b/internal/command/admin/deploy/kubernetes/kubernetes_up.go index af6c196..21535c5 100644 --- a/internal/command/admin/deploy/kubernetes/kubernetes_up.go +++ b/internal/command/admin/deploy/kubernetes/kubernetes_up.go @@ -1,4 +1,4 @@ -// Copyright 2022 Giuseppe De Palma, Matteo Trentin +// Copyright 2023 Giuseppe De Palma, Matteo Trentin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import ( "bytes" "context" + "github.com/funlessdev/fl-cli/pkg" + "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/deploy" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -39,9 +41,11 @@ EXAMPLES $ fl admin deploy kubernetes up --kubeconfig ` } -func (k *Up) Run(ctx context.Context, deployer deploy.KubernetesDeployer, logger log.FLogger) error { +func (k *Up) Run(ctx context.Context, deployer deploy.KubernetesDeployer, logger log.FLogger, config client.Config) error { logger.Info("Deploying FunLess on Kubernetes...\n\n") + ctx = context.WithValue(ctx, pkg.FLContextKey("secret_key_base"), config.SecretKeyBase) + _ = logger.StartSpinner("Setting things up...") if err := logger.StopSpinner(deployer.WithConfig(k.KubeConfig)); err != nil { return err @@ -97,6 +101,11 @@ func (k *Up) Run(ctx context.Context, deployer deploy.KubernetesDeployer, logger return err } + _ = logger.StartSpinner("Creating Core Secrets...") + if err := logger.StopSpinner(deployer.CreateCoreSecrets(ctx)); err != nil { + return err + } + _ = logger.StartSpinner("Deploying Core...") if err := logger.StopSpinner(deployer.DeployCore(ctx)); err != nil { return err @@ -112,6 +121,17 @@ func (k *Up) Run(ctx context.Context, deployer deploy.KubernetesDeployer, logger return err } + var cmdOut, cmdErr bytes.Buffer + _ = logger.StartSpinner("Extracting auth tokens...") + if err := logger.StopSpinner(deployer.ExtractTokens(ctx, &cmdOut, &cmdErr)); err != nil { + logger.Infof("\n%+v\n", err) + logger.Infof("\n%+v\n", cmdErr.String()) + logger.Info("Couldn't extract auth tokens from core pod. Completing deployment...") + } else { + logger.Infof("\n%+v\n", cmdOut.String()) + logger.Info("\nRemember to add these tokens in ~/.fl/config as api_token and admin_token.\n") + } + logger.Info("\nDeployment complete!\n") logger.Info("You can now start using FunLess! 🎉\n") diff --git a/internal/command/admin/deploy/kubernetes/kubernetes_up_test.go b/internal/command/admin/deploy/kubernetes/kubernetes_up_test.go index b5c4337..d9c4411 100644 --- a/internal/command/admin/deploy/kubernetes/kubernetes_up_test.go +++ b/internal/command/admin/deploy/kubernetes/kubernetes_up_test.go @@ -19,6 +19,7 @@ import ( "errors" "testing" + "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/test/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -35,7 +36,7 @@ func TestKubernetesUpRun(t *testing.T) { t.Run("should return error when setting up Deployer fails", func(t *testing.T) { mockDeployer.On("WithConfig", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "WithConfig", 1) }) @@ -44,7 +45,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("WithConfig", mock.Anything).Return(nil) mockDeployer.On("CreateNamespace", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "CreateNamespace", 1) }) @@ -53,7 +54,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("CreateNamespace", mock.Anything).Return(nil) mockDeployer.On("CreateSvcAccount", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "CreateSvcAccount", 1) }) @@ -62,7 +63,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("CreateSvcAccount", mock.Anything).Return(nil) mockDeployer.On("CreateRole", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "CreateRole", 1) }) @@ -71,7 +72,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("CreateRole", mock.Anything).Return(nil) mockDeployer.On("CreateRoleBinding", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "CreateRoleBinding", 1) }) @@ -80,7 +81,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("CreateRoleBinding", mock.Anything).Return(nil) mockDeployer.On("CreatePrometheusConfigMap", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "CreatePrometheusConfigMap", 1) }) @@ -89,7 +90,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("CreatePrometheusConfigMap", mock.Anything).Return(nil) mockDeployer.On("DeployPrometheus", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployPrometheus", 1) }) @@ -98,7 +99,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("DeployPrometheus", mock.Anything).Return(nil) mockDeployer.On("DeployPrometheusService", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployPrometheusService", 1) }) @@ -107,7 +108,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("DeployPrometheusService", mock.Anything).Return(nil) mockDeployer.On("DeployPostgres", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployPostgres", 1) }) @@ -116,7 +117,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("DeployPostgres", mock.Anything).Return(nil) mockDeployer.On("DeployPostgresService", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployPostgresService", 1) }) @@ -125,16 +126,25 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("DeployPostgresService", mock.Anything).Return(nil) mockDeployer.On("StartInitPostgres", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "StartInitPostgres", 1) }) - t.Run("should return error when deploying Core fails", func(t *testing.T) { + t.Run("should return error when create Core Secrets fails", func(t *testing.T) { mockDeployer.On("StartInitPostgres", mock.Anything).Return(nil) + mockDeployer.On("CreateCoreSecrets", mock.Anything).Return(errors.New("error")).Once() + + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) + require.Error(t, err) + mockDeployer.AssertNumberOfCalls(t, "CreateCoreSecrets", 1) + }) + + t.Run("should return error when deploying Core fails", func(t *testing.T) { + mockDeployer.On("CreateCoreSecrets", mock.Anything).Return(nil) mockDeployer.On("DeployCore", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployCore", 1) }) @@ -143,7 +153,7 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("DeployCore", mock.Anything).Return(nil) mockDeployer.On("DeployCoreService", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployCoreService", 1) }) @@ -152,16 +162,69 @@ func TestKubernetesUpRun(t *testing.T) { mockDeployer.On("DeployCoreService", mock.Anything).Return(nil) mockDeployer.On("DeployWorker", mock.Anything).Return(errors.New("error")).Once() - err := k8s.Run(ctx, mockDeployer, logger) + err := k8s.Run(ctx, mockDeployer, logger, client.Config{}) require.Error(t, err) mockDeployer.AssertNumberOfCalls(t, "DeployWorker", 1) }) - t.Run("successful prints when everything goes well", func(t *testing.T) { + t.Run("should show error and then complete deployment when extracting Tokens fails", func(t *testing.T) { mockDeployer.On("DeployWorker", mock.Anything).Return(nil) + mockDeployer.On("ExtractTokens", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("token error")).Once() + outbuf, testLogger := testLogger() + err := k8s.Run(ctx, mockDeployer, testLogger, client.Config{}) + + expectedOutput := `Deploying FunLess on Kubernetes... + +Setting things up... +done +Creating Namespace... +done +Creating ServiceAccount... +done +Creating Role... +done +Creating RoleBinding... +done +Creating Prometheus ConfigMap... +done +Deploying Prometheus... +done +Deploying Prometheus Service... +done +Deploying PostgreSQL... +done +Deploying PostgreSQL Service... +done +Starting init-postgres Job... +done +Creating Core Secrets... +done +Deploying Core... +done +Deploying Core Service... +done +Deploying Workers... +done +Extracting auth tokens... +failed + +token error + + +Couldn't extract auth tokens from core pod. Completing deployment... +Deployment complete! +You can now start using FunLess! 🎉 +` + assert.NoError(t, err) + assert.Equal(t, expectedOutput, outbuf.String()) + mockDeployer.AssertNumberOfCalls(t, "ExtractTokens", 1) + }) + + t.Run("successful prints when everything goes well", func(t *testing.T) { + mockDeployer.On("ExtractTokens", mock.Anything, mock.Anything, mock.Anything).Return(nil) outbuf, testLogger := testLogger() - err := k8s.Run(ctx, mockDeployer, testLogger) + err := k8s.Run(ctx, mockDeployer, testLogger, client.Config{}) expectedOutput := `Deploying FunLess on Kubernetes... @@ -187,12 +250,20 @@ Deploying PostgreSQL Service... done Starting init-postgres Job... done +Creating Core Secrets... +done Deploying Core... done Deploying Core Service... done Deploying Workers... done +Extracting auth tokens... +done + + + +Remember to add these tokens in ~/.fl/config as api_token and admin_token. Deployment complete! You can now start using FunLess! 🎉 diff --git a/internal/command/admin/user/user.go b/internal/command/admin/user/user.go index 8160542..975efc8 100644 --- a/internal/command/admin/user/user.go +++ b/internal/command/admin/user/user.go @@ -18,6 +18,7 @@ import ( "context" "fmt" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -25,21 +26,26 @@ import ( type User struct { Create CreateUser `cmd:"" name:"create" aliases:"c" help:"Create a new FunLess user"` List ListUsers `cmd:"" name:"list" aliases:"l" help:"List all FunLess users"` + + Host string `short:"H" help:"API host/port of the platform (no protocol)"` } type CreateUser struct { Name string `arg:"" name:"name" help:"The name of the new user"` } -func (u *CreateUser) Run(ctx context.Context, userHandler client.UserHandler, logger log.FLogger) error { - logger.StartSpinner("Creating user...") +func (u *CreateUser) Run(ctx context.Context, userHandler client.UserHandler, logger log.FLogger, parent *User) error { + + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) + + _ = logger.StartSpinner("Creating user...") res, err := userHandler.Create(ctx, u.Name) _ = logger.StopSpinner(err) if err != nil { return err } logger.Info(fmt.Sprintf("User %s created. Auth token:\n", res.Name)) - logger.Info(res.Token) + logger.Info(res.Token + "\n") return err } @@ -61,16 +67,19 @@ EXAMPLES type ListUsers struct { } -func (u *ListUsers) Run(ctx context.Context, userHandler client.UserHandler, logger log.FLogger) error { - logger.StartSpinner("Listing existing users...") +func (u *ListUsers) Run(ctx context.Context, userHandler client.UserHandler, logger log.FLogger, parent *User) error { + + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) + + _ = logger.StartSpinner("Listing existing users...") res, err := userHandler.List(ctx) _ = logger.StopSpinner(err) if err != nil { return err } - logger.Info("Users:") + logger.Info("Users:\n") for _, user := range res.Names { - logger.Info(fmt.Sprintf("- %s", user)) + logger.Info(fmt.Sprintf("- %s\n", user)) } return err } diff --git a/internal/command/admin/user/user_test.go b/internal/command/admin/user/user_test.go index ab4dbac..325b1e3 100644 --- a/internal/command/admin/user/user_test.go +++ b/internal/command/admin/user/user_test.go @@ -22,6 +22,7 @@ import ( "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -34,12 +35,12 @@ func TestCreateUser(t *testing.T) { var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).DisableAnimation().Build() mockUserHandler := mocks.NewUserHandler(t) - mockUserHandler.On("Create", ctx, "userA").Return(mockResult, nil) + mockUserHandler.On("Create", mock.Anything, "userA").Return(mockResult, nil) cmd := CreateUser{Name: "userA"} - err := cmd.Run(ctx, mockUserHandler, bufLogger) + err := cmd.Run(ctx, mockUserHandler, bufLogger, &User{}) require.NoError(t, err) - mockUserHandler.AssertCalled(t, "Create", ctx, "userA") + mockUserHandler.AssertCalled(t, "Create", mock.Anything, "userA") mockUserHandler.AssertNumberOfCalls(t, "Create", 1) mockUserHandler.AssertExpectations(t) @@ -53,12 +54,12 @@ func TestListUsers(t *testing.T) { var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).DisableAnimation().Build() mockUserHandler := mocks.NewUserHandler(t) - mockUserHandler.On("List", ctx).Return(mockResult, nil) + mockUserHandler.On("List", mock.Anything).Return(mockResult, nil) cmd := ListUsers{} - err := cmd.Run(ctx, mockUserHandler, bufLogger) + err := cmd.Run(ctx, mockUserHandler, bufLogger, &User{}) require.NoError(t, err) - mockUserHandler.AssertCalled(t, "List", ctx) + mockUserHandler.AssertCalled(t, "List", mock.Anything) mockUserHandler.AssertNumberOfCalls(t, "List", 1) mockUserHandler.AssertExpectations(t) diff --git a/internal/command/cfg/cfg.go b/internal/command/cfg/cfg.go new file mode 100644 index 0000000..104ed5a --- /dev/null +++ b/internal/command/cfg/cfg.go @@ -0,0 +1,98 @@ +// Copyright 2022 Giuseppe De Palma, Matteo Trentin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cfg + +import ( + "context" + "fmt" + "os" + "path" + "regexp" + "strings" + + "github.com/funlessdev/fl-cli/pkg" + "github.com/funlessdev/fl-cli/pkg/client" + "github.com/funlessdev/fl-cli/pkg/homedir" + "github.com/funlessdev/fl-cli/pkg/log" +) + +type Cfg struct { + Set CfgSet `cmd:"" name:"set" aliases:"s" help:"set a property in the config file"` + Get CfgGet `cmd:"" name:"get" aliases:"g" help:"get a property from the config file"` +} + +type CfgSet struct { + Key string `arg:"" enum:"${config_keys}" help:"name of the parameter that is being set"` + Value string `arg:"" help:"value of the parameter that is being set"` +} + +type CfgGet struct { + Key string `arg:"" enum:"${config_keys}" help:"name of the parameter that is being read"` +} + +func (g *CfgSet) Run(ctx context.Context, logger log.FLogger, config client.Config) error { + + var configBasePath string + + if config.Path == "" { + configBasePath = pkg.ConfigFileName + } else { + configBasePath = path.Base(config.Path) + } + + configText, _, err := homedir.ReadFromConfigDir(configBasePath) + + if err != nil && !os.IsNotExist(err) { + return err + } + + configString := string(configText[:]) + r, _ := regexp.Compile(fmt.Sprintf("%s=(.+)", g.Key)) + + var outConfig string + + if r.MatchString(configString) { + outConfig = r.ReplaceAllLiteralString(configString, fmt.Sprintf("%s=%s", g.Key, g.Value)) + } else { + outConfig = fmt.Sprintf("%s\n%s=%s\n", strings.Trim(configString, "\n"), g.Key, g.Value) + } + + _, err = homedir.WriteToConfigDir(configBasePath, []byte(outConfig), true) + if err != nil { + return err + } + + logger.Infof("Key %s set to %s in config (path %s).\n", g.Key, g.Value, config.Path) + + return nil +} + +func (g *CfgGet) Run(ctx context.Context, logger log.FLogger, config client.Config) error { + var cfgValue string + switch g.Key { + case "api_host": + cfgValue = config.Host + case "api_token": + cfgValue = config.APIToken + case "admin_token": + cfgValue = config.AdminToken + case "secret_key_base": + cfgValue = config.SecretKeyBase + } + + logger.Infof("%s=%s\n", g.Key, cfgValue) + + return nil +} diff --git a/internal/command/cfg/cfg_test.go b/internal/command/cfg/cfg_test.go new file mode 100644 index 0000000..eca9dc6 --- /dev/null +++ b/internal/command/cfg/cfg_test.go @@ -0,0 +1,104 @@ +// Copyright 2022 Giuseppe De Palma, Matteo Trentin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cfg + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/funlessdev/fl-cli/pkg/client" + "github.com/funlessdev/fl-cli/pkg/homedir" + "github.com/funlessdev/fl-cli/pkg/log" + "github.com/stretchr/testify/require" +) + +func TestCfg(t *testing.T) { + var outbuf bytes.Buffer + testCtx := context.Background() + testSetLogger, _ := log.NewLoggerBuilder().WithWriter(os.Stdout).Build() + testGetLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).DisableAnimation().Build() + + homedirPath, err := os.MkdirTemp("", "funless-test-cfg-") + require.NoError(t, err) + + homedir.GetHomeDir = func() (string, error) { + return homedirPath, nil + } + + defer func() { + homedir.GetHomeDir = os.UserHomeDir + os.RemoveAll(homedirPath) + }() + + config, err := client.NewConfig("config") + require.NoError(t, err) + + setCmds := [4]CfgSet{ + { + Key: "api_host", + Value: "test_host", + }, + { + Key: "api_token", + Value: "test_api_token", + }, + { + Key: "admin_token", + Value: "test_admin_token", + }, + { + Key: "secret_key_base", + Value: "test_secret_key_base", + }, + } + + for _, c := range setCmds { + err = c.Run(testCtx, testSetLogger, config) + require.NoError(t, err) + } + + config, err = client.NewConfig("config") + require.NoError(t, err) + + getCmds := [4]CfgGet{ + { + Key: "api_host", + }, + { + Key: "api_token", + }, + { + Key: "admin_token", + }, + { + Key: "secret_key_base", + }, + } + + for _, c := range getCmds { + err = c.Run(testCtx, testGetLogger, config) + require.NoError(t, err) + } + + expected := + "api_host=test_host\n" + + "api_token=test_api_token\n" + + "admin_token=test_admin_token\n" + + "secret_key_base=test_secret_key_base\n" + + require.Equal(t, expected, outbuf.String()) +} diff --git a/internal/command/fn/create.go b/internal/command/fn/create.go index 275e101..b6c6c08 100644 --- a/internal/command/fn/create.go +++ b/internal/command/fn/create.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/build" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" @@ -49,7 +50,10 @@ EXAMPLES } -func (c *Create) Run(ctx context.Context, builder build.DockerBuilder, fnHandler client.FnHandler, logger log.FLogger) error { +func (c *Create) Run(ctx context.Context, builder build.DockerBuilder, fnHandler client.FnHandler, logger log.FLogger, parent *Fn) error { + + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) + logger.Infof("Creating %s function...\n\n", c.Name) _ = logger.StartSpinner("Building function...🏗 ️") diff --git a/internal/command/fn/create_test.go b/internal/command/fn/create_test.go index d3f78da..f428196 100644 --- a/internal/command/fn/create_test.go +++ b/internal/command/fn/create_test.go @@ -49,12 +49,12 @@ Successfully created function %s/%s. testLogger, _ := log.NewLoggerBuilder().WithWriter(os.Stdout).DisableAnimation().Build() mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Create", ctx, testFn, testMod, mock.Anything).Return(nil) + mockFnHandler.On("Create", mock.Anything, testFn, testMod, mock.Anything).Return(nil) mockBuilder := mocks.NewDockerBuilder(t) mockBuilder.On("Setup", mock.Anything, testLanguage, mock.Anything).Return(nil) - mockBuilder.On("PullBuilderImage", ctx).Return(nil) - mockBuilder.On("BuildSource", ctx, testDir).Return(nil) + mockBuilder.On("PullBuilderImage", mock.Anything).Return(nil) + mockBuilder.On("BuildSource", mock.Anything, testDir).Return(nil) // monkey patch the openWasmFile function openWasmFile = func(path string) (*os.File, error) { @@ -73,11 +73,11 @@ Successfully created function %s/%s. bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).DisableAnimation().Build() - err := cmd.Run(ctx, mockBuilder, mockFnHandler, bufLogger) + err := cmd.Run(ctx, mockBuilder, mockFnHandler, bufLogger, &Fn{}) require.NoError(t, err) assert.Equal(t, testResult, (&outbuf).String()) - mockFnHandler.AssertCalled(t, "Create", ctx, testFn, testMod, mock.AnythingOfType("*os.File")) + mockFnHandler.AssertCalled(t, "Create", mock.Anything, testFn, testMod, mock.AnythingOfType("*os.File")) mockFnHandler.AssertNumberOfCalls(t, "Create", 1) mockFnHandler.AssertExpectations(t) mockBuilder.AssertExpectations(t) @@ -91,10 +91,10 @@ Successfully created function %s/%s. Language: testLanguage, } - err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger) + err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger, &Fn{}) require.NoError(t, err) - mockBuilder.AssertCalled(t, "BuildSource", ctx, testDir) + mockBuilder.AssertCalled(t, "BuildSource", mock.Anything, testDir) mockBuilder.AssertNumberOfCalls(t, "BuildSource", 2) mockBuilder.AssertExpectations(t) }) @@ -110,7 +110,7 @@ Successfully created function %s/%s. mockBuilder := mocks.NewDockerBuilder(t) mockBuilder.On("Setup", mock.Anything, testLanguage, mock.Anything).Return(errors.New("some error")).Once() - err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger) + err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) mockBuilder.AssertExpectations(t) }) @@ -125,9 +125,9 @@ Successfully created function %s/%s. mockBuilder := mocks.NewDockerBuilder(t) mockBuilder.On("Setup", mock.Anything, testLanguage, mock.Anything).Return(nil).Once() - mockBuilder.On("PullBuilderImage", ctx).Return(errors.New("some error")).Once() + mockBuilder.On("PullBuilderImage", mock.Anything).Return(errors.New("some error")).Once() - err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger) + err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) mockBuilder.AssertExpectations(t) }) @@ -142,10 +142,10 @@ Successfully created function %s/%s. mockBuilder := mocks.NewDockerBuilder(t) mockBuilder.On("Setup", mock.Anything, testLanguage, mock.Anything).Return(nil).Once() - mockBuilder.On("PullBuilderImage", ctx).Return(nil).Once() - mockBuilder.On("BuildSource", ctx, testDir).Return(errors.New("some error")).Once() + mockBuilder.On("PullBuilderImage", mock.Anything).Return(nil).Once() + mockBuilder.On("BuildSource", mock.Anything, testDir).Return(errors.New("some error")).Once() - err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger) + err := cmd.Run(ctx, mockBuilder, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) mockBuilder.AssertExpectations(t) }) diff --git a/internal/command/fn/delete.go b/internal/command/fn/delete.go index 5e55b41..c80d1bf 100644 --- a/internal/command/fn/delete.go +++ b/internal/command/fn/delete.go @@ -17,6 +17,7 @@ package fn import ( "context" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -41,7 +42,10 @@ EXAMPLES } -func (f *Delete) Run(ctx context.Context, fnHandler client.FnHandler, logger log.FLogger) error { +func (f *Delete) Run(ctx context.Context, fnHandler client.FnHandler, logger log.FLogger, parent *Fn) error { + + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) + err := fnHandler.Delete(ctx, f.Name, f.Module) if err != nil { return err diff --git a/internal/command/fn/delete_test.go b/internal/command/fn/delete_test.go index 353625c..98c05af 100644 --- a/internal/command/fn/delete_test.go +++ b/internal/command/fn/delete_test.go @@ -23,6 +23,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" @@ -42,11 +43,11 @@ func TestFnDelete(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Delete", testCtx, testFn, testMod).Return(nil) + mockFnHandler.On("Delete", mock.Anything, testFn, testMod).Return(nil) - err := cmd.Run(testCtx, mockFnHandler, testLogger) + err := cmd.Run(testCtx, mockFnHandler, testLogger, &Fn{}) require.NoError(t, err) - mockFnHandler.AssertCalled(t, "Delete", testCtx, testFn, testMod) + mockFnHandler.AssertCalled(t, "Delete", mock.Anything, testFn, testMod) mockFnHandler.AssertNumberOfCalls(t, "Delete", 1) mockFnHandler.AssertExpectations(t) }) @@ -57,12 +58,12 @@ func TestFnDelete(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Delete", testCtx, testFn, testMod).Return(nil) + mockFnHandler.On("Delete", mock.Anything, testFn, testMod).Return(nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockFnHandler, bufLogger) + err := cmd.Run(testCtx, mockFnHandler, bufLogger, &Fn{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("\nSuccessfully deleted function %s/%s.\n", testMod, testFn), (&outbuf).String()) @@ -78,9 +79,9 @@ func TestFnDelete(t *testing.T) { mockFnHandler := mocks.NewFnHandler(t) e := &openapi.GenericOpenAPIError{} - mockFnHandler.On("Delete", testCtx, testFn, testMod).Return(e) + mockFnHandler.On("Delete", mock.Anything, testFn, testMod).Return(e) - err := cmd.Run(testCtx, mockFnHandler, testLogger) + err := cmd.Run(testCtx, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) }) } diff --git a/internal/command/fn/fn.go b/internal/command/fn/fn.go index 881ebf3..8fdd83e 100644 --- a/internal/command/fn/fn.go +++ b/internal/command/fn/fn.go @@ -21,4 +21,6 @@ type Fn struct { Build Build `cmd:"" aliases:"b" help:"Compile a function into a wasm binary"` Upload Upload `cmd:"" aliases:"up" help:"Create functions by uploading wasm binaries"` New New `cmd:"" aliases:"n" help:"Create a new function from a template"` + + Host string `short:"H" help:"API host/port of the platform (no protocol)"` } diff --git a/internal/command/fn/invoke.go b/internal/command/fn/invoke.go index ac7a8e1..b4097ce 100644 --- a/internal/command/fn/invoke.go +++ b/internal/command/fn/invoke.go @@ -18,13 +18,14 @@ import ( "context" "encoding/json" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) type Invoke struct { Name string `arg:"" name:"name" help:"Name of the function to invoke"` - Module string `name:"module" short:"n" default:"_" help:"Module of the function to invoke"` + Module string `name:"module" short:"m" default:"_" help:"Module of the function to invoke"` Args map[string]string `name:"args" short:"a" help:"Arguments of the function to invoke" xor:"args"` JsonArgs string `name:"json" short:"j" help:"Json encoded arguments of the function to invoke; overrides args" xor:"args"` } @@ -44,7 +45,10 @@ EXAMPLES ` } -func (f *Invoke) Run(ctx context.Context, fnHandler client.FnHandler, logger log.FLogger) error { +func (f *Invoke) Run(ctx context.Context, fnHandler client.FnHandler, logger log.FLogger, parent *Fn) error { + + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) + args := make(map[string]interface{}, len(f.Args)) if f.Args != nil { for k, v := range f.Args { diff --git a/internal/command/fn/invoke_test.go b/internal/command/fn/invoke_test.go index e9f34eb..5e96b0e 100644 --- a/internal/command/fn/invoke_test.go +++ b/internal/command/fn/invoke_test.go @@ -48,11 +48,11 @@ func TestFnInvoke(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Invoke", testCtx, testFn, testMod, map[string]interface{}{}).Return(pkg.IvkResult{Result: testResult}, nil) + mockFnHandler.On("Invoke", mock.Anything, testFn, testMod, map[string]interface{}{}).Return(pkg.IvkResult{Result: testResult}, nil) - err := cmd.Run(testCtx, mockFnHandler, testLogger) + err := cmd.Run(testCtx, mockFnHandler, testLogger, &Fn{}) require.NoError(t, err) - mockFnHandler.AssertCalled(t, "Invoke", testCtx, testFn, testMod, map[string]interface{}{}) + mockFnHandler.AssertCalled(t, "Invoke", mock.Anything, testFn, testMod, map[string]interface{}{}) mockFnHandler.AssertNumberOfCalls(t, "Invoke", 1) mockFnHandler.AssertExpectations(t) }) @@ -65,12 +65,12 @@ func TestFnInvoke(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Invoke", testCtx, testFn, testMod, map[string]interface{}{}).Return(pkg.IvkResult{Result: testResult}, nil) + mockFnHandler.On("Invoke", mock.Anything, testFn, testMod, map[string]interface{}{}).Return(pkg.IvkResult{Result: testResult}, nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockFnHandler, bufLogger) + err := cmd.Run(testCtx, mockFnHandler, bufLogger, &Fn{}) require.NoError(t, err) assert.Equal(t, testResult, (&outbuf).String()) @@ -90,11 +90,11 @@ func TestFnInvoke(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Invoke", testCtx, testFn, testMod, mockArgs).Return(pkg.IvkResult{Result: testResult}, nil) + mockFnHandler.On("Invoke", mock.Anything, testFn, testMod, mockArgs).Return(pkg.IvkResult{Result: testResult}, nil) - err := cmd.Run(testCtx, mockFnHandler, testLogger) + err := cmd.Run(testCtx, mockFnHandler, testLogger, &Fn{}) require.NoError(t, err) - mockFnHandler.AssertCalled(t, "Invoke", testCtx, testFn, testMod, mockArgs) + mockFnHandler.AssertCalled(t, "Invoke", mock.Anything, testFn, testMod, mockArgs) mockFnHandler.AssertNumberOfCalls(t, "Invoke", 1) mockFnHandler.AssertExpectations(t) }) @@ -107,12 +107,12 @@ func TestFnInvoke(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Invoke", testCtx, testFn, testMod, testParsedJArgs).Return( + mockFnHandler.On("Invoke", mock.Anything, testFn, testMod, testParsedJArgs).Return( pkg.IvkResult{Result: testResult}, nil) - err := cmd.Run(testCtx, mockFnHandler, testLogger) + err := cmd.Run(testCtx, mockFnHandler, testLogger, &Fn{}) require.NoError(t, err) - mockFnHandler.AssertCalled(t, "Invoke", testCtx, testFn, testMod, testParsedJArgs) + mockFnHandler.AssertCalled(t, "Invoke", mock.Anything, testFn, testMod, testParsedJArgs) mockFnHandler.AssertNumberOfCalls(t, "Invoke", 1) mockFnHandler.AssertExpectations(t) }) @@ -125,10 +125,10 @@ func TestFnInvoke(t *testing.T) { mockFnHandler := mocks.NewFnHandler(t) e := &openapi.GenericOpenAPIError{} - mockFnHandler.On("Invoke", testCtx, testFn, "", mock.Anything).Return( + mockFnHandler.On("Invoke", mock.Anything, testFn, "", mock.Anything).Return( pkg.IvkResult{}, e) - err := cmd.Run(testCtx, mockFnHandler, testLogger) + err := cmd.Run(testCtx, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) }) } diff --git a/internal/command/fn/upload.go b/internal/command/fn/upload.go index 840c8a1..528091e 100644 --- a/internal/command/fn/upload.go +++ b/internal/command/fn/upload.go @@ -24,6 +24,7 @@ import ( "path/filepath" "strings" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -49,7 +50,10 @@ EXAMPLES ` } -func (u *Upload) Run(ctx context.Context, fnHandler client.FnHandler, logger log.FLogger) error { +func (u *Upload) Run(ctx context.Context, fnHandler client.FnHandler, logger log.FLogger, parent *Fn) error { + + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) + _ = logger.StartSpinner("Reading wasm...") code, err := openWasmFile(u.Source) if err != nil { diff --git a/internal/command/fn/upload_test.go b/internal/command/fn/upload_test.go index 01a4562..5b4d724 100644 --- a/internal/command/fn/upload_test.go +++ b/internal/command/fn/upload_test.go @@ -46,7 +46,7 @@ func TestFnUpload(t *testing.T) { Module: testMod, } mockFnHandler := mocks.NewFnHandler(t) - err := upload.Run(ctx, mockFnHandler, testLogger) + err := upload.Run(ctx, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) }) @@ -58,12 +58,12 @@ func TestFnUpload(t *testing.T) { } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Create", ctx, testFn, testMod, mock.Anything).Return(errors.New("error")).Once() + mockFnHandler.On("Create", mock.Anything, testFn, testMod, mock.Anything).Return(errors.New("error")).Once() - err := cmd.Run(ctx, mockFnHandler, testLogger) + err := cmd.Run(ctx, mockFnHandler, testLogger, &Fn{}) require.Error(t, err) - mockFnHandler.AssertCalled(t, "Create", ctx, testFn, testMod, mock.AnythingOfType("*os.File")) + mockFnHandler.AssertCalled(t, "Create", mock.Anything, testFn, testMod, mock.AnythingOfType("*os.File")) mockFnHandler.AssertNumberOfCalls(t, "Create", 1) mockFnHandler.AssertExpectations(t) }) @@ -85,12 +85,12 @@ Successfully uploaded function %s/%s 👌 } mockFnHandler := mocks.NewFnHandler(t) - mockFnHandler.On("Create", ctx, testFn, testMod, mock.Anything).Return(nil) + mockFnHandler.On("Create", mock.Anything, testFn, testMod, mock.Anything).Return(nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).DisableAnimation().Build() - err := cmd.Run(ctx, mockFnHandler, bufLogger) + err := cmd.Run(ctx, mockFnHandler, bufLogger, &Fn{}) require.NoError(t, err) assert.Equal(t, testResult, (&outbuf).String()) diff --git a/internal/command/mod/create.go b/internal/command/mod/create.go index 23931a8..3c2116b 100644 --- a/internal/command/mod/create.go +++ b/internal/command/mod/create.go @@ -17,6 +17,7 @@ package mod import ( "context" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -47,8 +48,10 @@ EXAMPLES } -func (c *Create) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger) error { +func (c *Create) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger, parent *Mod) error { + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) err := modHandler.Create(ctx, c.Name) + if err != nil { return err } diff --git a/internal/command/mod/create_test.go b/internal/command/mod/create_test.go index a71c0c2..1099082 100644 --- a/internal/command/mod/create_test.go +++ b/internal/command/mod/create_test.go @@ -24,6 +24,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" openapi "github.com/funlessdev/fl-client-sdk-go" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) @@ -39,11 +40,11 @@ func TestModCreate(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Create", testCtx, testMod).Return(nil) + mockModHandler.On("Create", mock.Anything, testMod).Return(nil) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.NoError(t, err) - mockModHandler.AssertCalled(t, "Create", testCtx, testMod) + mockModHandler.AssertCalled(t, "Create", mock.Anything, testMod) mockModHandler.AssertNumberOfCalls(t, "Create", 1) mockModHandler.AssertExpectations(t) }) @@ -53,12 +54,12 @@ func TestModCreate(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Create", testCtx, testMod).Return(nil) + mockModHandler.On("Create", mock.Anything, testMod).Return(nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("Successfully created module %s.\n", testMod), (&outbuf).String()) @@ -73,9 +74,9 @@ func TestModCreate(t *testing.T) { mockModHandler := mocks.NewModHandler(t) e := &openapi.GenericOpenAPIError{} - mockModHandler.On("Create", testCtx, testMod).Return(e) + mockModHandler.On("Create", mock.Anything, testMod).Return(e) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.Error(t, err) }) } diff --git a/internal/command/mod/delete.go b/internal/command/mod/delete.go index e38f14e..ff64c9d 100644 --- a/internal/command/mod/delete.go +++ b/internal/command/mod/delete.go @@ -17,6 +17,7 @@ package mod import ( "context" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -45,7 +46,8 @@ EXAMPLES ` } -func (d *Delete) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger) error { +func (d *Delete) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger, parent *Mod) error { + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) err := modHandler.Delete(ctx, d.Name) if err != nil { diff --git a/internal/command/mod/delete_test.go b/internal/command/mod/delete_test.go index 4baa610..b3a6ea7 100644 --- a/internal/command/mod/delete_test.go +++ b/internal/command/mod/delete_test.go @@ -24,6 +24,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" openapi "github.com/funlessdev/fl-client-sdk-go" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) @@ -39,11 +40,11 @@ func TestModDelete(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Delete", testCtx, testMod).Return(nil) + mockModHandler.On("Delete", mock.Anything, testMod).Return(nil) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.NoError(t, err) - mockModHandler.AssertCalled(t, "Delete", testCtx, testMod) + mockModHandler.AssertCalled(t, "Delete", mock.Anything, testMod) mockModHandler.AssertNumberOfCalls(t, "Delete", 1) mockModHandler.AssertExpectations(t) }) @@ -53,12 +54,12 @@ func TestModDelete(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Delete", testCtx, testMod).Return(nil) + mockModHandler.On("Delete", mock.Anything, testMod).Return(nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("Successfully deleted module %s.\n", testMod), (&outbuf).String()) @@ -73,9 +74,9 @@ func TestModDelete(t *testing.T) { mockModHandler := mocks.NewModHandler(t) e := &openapi.GenericOpenAPIError{} - mockModHandler.On("Delete", testCtx, testMod).Return(e) + mockModHandler.On("Delete", mock.Anything, testMod).Return(e) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.Error(t, err) }) } diff --git a/internal/command/mod/get.go b/internal/command/mod/get.go index 06355a9..d57be3c 100644 --- a/internal/command/mod/get.go +++ b/internal/command/mod/get.go @@ -17,6 +17,7 @@ package mod import ( "context" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -39,8 +40,10 @@ EXAMPLES ` } -func (g *Get) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger) error { +func (g *Get) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger, parent *Mod) error { + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) res, err := modHandler.Get(ctx, g.Name) + if err != nil { return err } diff --git a/internal/command/mod/get_test.go b/internal/command/mod/get_test.go index c52bae8..33afa86 100644 --- a/internal/command/mod/get_test.go +++ b/internal/command/mod/get_test.go @@ -25,6 +25,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" openapi "github.com/funlessdev/fl-client-sdk-go" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) @@ -44,15 +45,15 @@ func TestModGet(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Get", testCtx, testMod).Return( + mockModHandler.On("Get", mock.Anything, testMod).Return( pkg.SingleModule{ Name: testMod, Functions: testFns, }, nil) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.NoError(t, err) - mockModHandler.AssertCalled(t, "Get", testCtx, testMod) + mockModHandler.AssertCalled(t, "Get", mock.Anything, testMod) mockModHandler.AssertNumberOfCalls(t, "Get", 1) mockModHandler.AssertExpectations(t) }) @@ -63,7 +64,7 @@ func TestModGet(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Get", testCtx, testMod).Return( + mockModHandler.On("Get", mock.Anything, testMod).Return( pkg.SingleModule{ Name: testMod, Functions: testFns, @@ -72,7 +73,7 @@ func TestModGet(t *testing.T) { var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("Module: %s\nFunctions:\n%s\n%s\n%s\n", testMod, testFns[0], testFns[1], testFns[2]), (&outbuf).String()) @@ -86,7 +87,7 @@ func TestModGet(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Get", testCtx, testMod).Return( + mockModHandler.On("Get", mock.Anything, testMod).Return( pkg.SingleModule{ Name: testMod, Functions: testFns, @@ -95,7 +96,7 @@ func TestModGet(t *testing.T) { var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("Module: %s\nFunctions:\n%s\n%s\n%s\nCount: %d\n", testMod, testFns[0], testFns[1], testFns[2], len(testFns)), (&outbuf).String()) @@ -110,9 +111,9 @@ func TestModGet(t *testing.T) { mockModHandler := mocks.NewModHandler(t) e := &openapi.GenericOpenAPIError{} - mockModHandler.On("Get", testCtx, testMod).Return(pkg.SingleModule{}, e) + mockModHandler.On("Get", mock.Anything, testMod).Return(pkg.SingleModule{}, e) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.Error(t, err) }) diff --git a/internal/command/mod/list.go b/internal/command/mod/list.go index 8cef466..dcc8ade 100644 --- a/internal/command/mod/list.go +++ b/internal/command/mod/list.go @@ -17,6 +17,7 @@ package mod import ( "context" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -38,8 +39,10 @@ EXAMPLES ` } -func (l *List) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger) error { +func (l *List) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger, parent *Mod) error { + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) res, err := modHandler.List(ctx) + if err != nil { return err } diff --git a/internal/command/mod/list_test.go b/internal/command/mod/list_test.go index d16716a..5ee846a 100644 --- a/internal/command/mod/list_test.go +++ b/internal/command/mod/list_test.go @@ -25,6 +25,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" openapi "github.com/funlessdev/fl-client-sdk-go" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) @@ -41,11 +42,11 @@ func TestModList(t *testing.T) { cmd := List{} mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("List", testCtx).Return(pkg.ModuleNameList{Names: testMods}, nil) + mockModHandler.On("List", mock.Anything).Return(pkg.ModuleNameList{Names: testMods}, nil) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.NoError(t, err) - mockModHandler.AssertCalled(t, "List", testCtx) + mockModHandler.AssertCalled(t, "List", mock.Anything) mockModHandler.AssertNumberOfCalls(t, "List", 1) mockModHandler.AssertExpectations(t) }) @@ -54,12 +55,12 @@ func TestModList(t *testing.T) { cmd := List{} mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("List", testCtx).Return(pkg.ModuleNameList{Names: testMods}, nil) + mockModHandler.On("List", mock.Anything).Return(pkg.ModuleNameList{Names: testMods}, nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("%s\n%s\n%s\n", testMods[0], testMods[1], testMods[2]), (&outbuf).String()) @@ -72,12 +73,12 @@ func TestModList(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("List", testCtx).Return(pkg.ModuleNameList{Names: testMods}, nil) + mockModHandler.On("List", mock.Anything).Return(pkg.ModuleNameList{Names: testMods}, nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("%s\n%s\n%s\nCount: %d\n", testMods[0], testMods[1], testMods[2], len(testMods)), (&outbuf).String()) @@ -90,9 +91,9 @@ func TestModList(t *testing.T) { mockModHandler := mocks.NewModHandler(t) e := &openapi.GenericOpenAPIError{} - mockModHandler.On("List", testCtx).Return(pkg.ModuleNameList{}, e) + mockModHandler.On("List", mock.Anything).Return(pkg.ModuleNameList{}, e) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.Error(t, err) }) } diff --git a/internal/command/mod/mod.go b/internal/command/mod/mod.go index 4f25645..b97c305 100644 --- a/internal/command/mod/mod.go +++ b/internal/command/mod/mod.go @@ -20,4 +20,6 @@ type Mod struct { Update Update `cmd:"" aliases:"u,up" help:"Update the name of a module"` Create Create `cmd:"" aliases:"c" help:"Create a new module"` List List `cmd:"" aliases:"l,ls" help:"List all modules"` + + Host string `short:"H" help:"API host/port of the platform (no protocol)"` } diff --git a/internal/command/mod/update.go b/internal/command/mod/update.go index cb6c3bb..566d2e7 100644 --- a/internal/command/mod/update.go +++ b/internal/command/mod/update.go @@ -17,6 +17,7 @@ package mod import ( "context" + "github.com/funlessdev/fl-cli/pkg" "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/log" ) @@ -38,8 +39,10 @@ EXAMPLES ` } -func (u *Update) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger) error { +func (u *Update) Run(ctx context.Context, modHandler client.ModHandler, logger log.FLogger, parent *Mod) error { + ctx = context.WithValue(ctx, pkg.FLContextKey("api_host"), parent.Host) err := modHandler.Update(ctx, u.Name, u.NewName) + if err != nil { return err } diff --git a/internal/command/mod/update_test.go b/internal/command/mod/update_test.go index 79b458f..ec362fa 100644 --- a/internal/command/mod/update_test.go +++ b/internal/command/mod/update_test.go @@ -24,6 +24,7 @@ import ( "github.com/funlessdev/fl-cli/pkg/log" "github.com/funlessdev/fl-cli/test/mocks" openapi "github.com/funlessdev/fl-client-sdk-go" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) @@ -41,11 +42,11 @@ func TestModUpdate(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Update", testCtx, testMod, testNewMod).Return(nil) + mockModHandler.On("Update", mock.Anything, testMod, testNewMod).Return(nil) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.NoError(t, err) - mockModHandler.AssertCalled(t, "Update", testCtx, testMod, testNewMod) + mockModHandler.AssertCalled(t, "Update", mock.Anything, testMod, testNewMod) mockModHandler.AssertNumberOfCalls(t, "Update", 1) mockModHandler.AssertExpectations(t) }) @@ -56,12 +57,12 @@ func TestModUpdate(t *testing.T) { } mockModHandler := mocks.NewModHandler(t) - mockModHandler.On("Update", testCtx, testMod, testNewMod).Return(nil) + mockModHandler.On("Update", mock.Anything, testMod, testNewMod).Return(nil) var outbuf bytes.Buffer bufLogger, _ := log.NewLoggerBuilder().WithWriter(&outbuf).Build() - err := cmd.Run(testCtx, mockModHandler, bufLogger) + err := cmd.Run(testCtx, mockModHandler, bufLogger, &Mod{}) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("Successfully renamed module %s to %s.\n", testMod, testNewMod), (&outbuf).String()) @@ -77,9 +78,9 @@ func TestModUpdate(t *testing.T) { mockModHandler := mocks.NewModHandler(t) e := &openapi.GenericOpenAPIError{} - mockModHandler.On("Update", testCtx, testMod, testNewMod).Return(e) + mockModHandler.On("Update", mock.Anything, testMod, testNewMod).Return(e) - err := cmd.Run(testCtx, mockModHandler, testLogger) + err := cmd.Run(testCtx, mockModHandler, testLogger, &Mod{}) require.Error(t, err) }) } diff --git a/pkg/client/client.go b/pkg/client/client.go index e0a220e..5d83f4a 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -15,11 +15,16 @@ package client import ( + "bufio" + "bytes" "errors" "fmt" "net/http" "net/url" + "os" + "strings" + "github.com/funlessdev/fl-cli/pkg/homedir" openapi "github.com/funlessdev/fl-client-sdk-go" ) @@ -34,8 +39,65 @@ type Client struct { } type Config struct { - Host string - BaseURL *url.URL + Path string + Host string + BaseURL *url.URL + SecretKeyBase string // used when deploying the platform, unused when using API + AdminToken string + APIToken string +} + +// NewConfig creates a new funless config, reading the information from the given configPath +func NewConfig(configPath string) (Config, error) { + + config, path, err := homedir.ReadFromConfigDir(configPath) + + outConfig := Config{ + Host: "http://localhost:4000", + SecretKeyBase: "", + AdminToken: "", + APIToken: "", + } + + if err != nil { + if os.IsNotExist(err) { + return outConfig, nil + } else { + return Config{}, err + } + } + configReader := bytes.NewReader(config) + configScanner := bufio.NewScanner(configReader) + configMap := make(map[string]string) + for configScanner.Scan() { + line := configScanner.Text() + lineParts := strings.Split(line, "=") + if len(lineParts) == 2 { + key, value := strings.TrimSpace(lineParts[0]), strings.TrimSpace(lineParts[1]) + configMap[key] = value + } + } + + if err = configScanner.Err(); err != nil { + return Config{}, err + } + + outConfig.Path = path + + if host, v := configMap["api_host"]; v { + outConfig.Host = host + } + if secretKeyBase, v := configMap["secret_key_base"]; v { + outConfig.SecretKeyBase = secretKeyBase + } + if adminToken, v := configMap["admin_token"]; v { + outConfig.AdminToken = adminToken + } + if apiToken, v := configMap["api_token"]; v { + outConfig.APIToken = apiToken + } + + return outConfig, nil } // NewClient creates a new funless client with the provided http client and configuration. diff --git a/pkg/client/fn_service.go b/pkg/client/fn_service.go index f6e7b61..5d82526 100644 --- a/pkg/client/fn_service.go +++ b/pkg/client/fn_service.go @@ -38,8 +38,27 @@ type FnService struct { var _ FnHandler = &FnService{} +func (fn *FnService) injectAPIToken() { + if fn.Client != nil { + apiToken := fn.Client.Config.APIToken + apiConfig := fn.Client.ApiClient.GetConfig() + apiConfig.DefaultHeader["Authorization"] = "Bearer " + apiToken + } +} + +func (fn *FnService) injectHost(ctx context.Context) { + overrideHost, ok := ctx.Value(pkg.FLContextKey("api_host")).(string) + if ok && overrideHost != "" { + apiConfig := fn.Client.ApiClient.GetConfig() + apiConfig.Host = overrideHost + } +} + func (fn *FnService) Invoke(ctx context.Context, fnName string, fnMod string, fnArgs map[string]interface{}) (pkg.IvkResult, error) { + fn.injectHost(ctx) + fn.injectAPIToken() + if err := fn.InputValidatorHandler.ValidateName(fnName, "function"); err != nil { return pkg.IvkResult{}, err } @@ -73,6 +92,9 @@ func (fn *FnService) Invoke(ctx context.Context, fnName string, fnMod string, fn func (fn *FnService) Create(ctx context.Context, fnName string, fnMod string, code *os.File) error { + fn.injectHost(ctx) + fn.injectAPIToken() + if err := fn.InputValidatorHandler.ValidateName(fnName, "function"); err != nil { return err } @@ -88,6 +110,9 @@ func (fn *FnService) Create(ctx context.Context, fnName string, fnMod string, co func (fn *FnService) Delete(ctx context.Context, fnName string, fnMod string) error { + fn.injectHost(ctx) + fn.injectAPIToken() + if err := fn.InputValidatorHandler.ValidateName(fnName, "function"); err != nil { return err } @@ -103,6 +128,9 @@ func (fn *FnService) Delete(ctx context.Context, fnName string, fnMod string) er func (fn *FnService) Update(ctx context.Context, fnName string, fnMod string, code *os.File, newName string) error { + fn.injectHost(ctx) + fn.injectAPIToken() + if err := fn.InputValidatorHandler.ValidateName(fnName, "function"); err != nil { return err } diff --git a/pkg/client/mod_service.go b/pkg/client/mod_service.go index 091a7ef..c3b4f10 100644 --- a/pkg/client/mod_service.go +++ b/pkg/client/mod_service.go @@ -36,13 +36,32 @@ type ModService struct { var _ ModHandler = &ModService{} -func (fn *ModService) Get(ctx context.Context, modName string) (pkg.SingleModule, error) { +func (mod *ModService) injectAPIToken() { + if mod.Client != nil { + apiToken := mod.Client.Config.APIToken + apiConfig := mod.Client.ApiClient.GetConfig() + apiConfig.DefaultHeader["Authorization"] = "Bearer " + apiToken + } +} + +func (mod *ModService) injectHost(ctx context.Context) { + overrideHost, ok := ctx.Value(pkg.FLContextKey("api_host")).(string) + if ok && overrideHost != "" { + apiConfig := mod.Client.ApiClient.GetConfig() + apiConfig.Host = overrideHost + } +} + +func (mod *ModService) Get(ctx context.Context, modName string) (pkg.SingleModule, error) { - if err := fn.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { + mod.injectHost(ctx) + mod.injectAPIToken() + + if err := mod.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { return pkg.SingleModule{}, err } - apiService := fn.Client.ApiClient.ModulesApi + apiService := mod.Client.ApiClient.ModulesApi request := apiService.ShowModuleByName(ctx, modName) response, _, err := request.Execute() if err != nil { @@ -52,8 +71,8 @@ func (fn *ModService) Get(ctx context.Context, modName string) (pkg.SingleModule name := data.Name var functions []string - for _, fn := range data.Functions { - functions = append(functions, *fn.Name) + for _, mod := range data.Functions { + functions = append(functions, *mod.Name) } return pkg.SingleModule{ @@ -63,13 +82,16 @@ func (fn *ModService) Get(ctx context.Context, modName string) (pkg.SingleModule } -func (fn *ModService) Create(ctx context.Context, modName string) error { +func (mod *ModService) Create(ctx context.Context, modName string) error { + + mod.injectHost(ctx) + mod.injectAPIToken() - if err := fn.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { + if err := mod.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { return err } - apiService := fn.Client.ApiClient.ModulesApi + apiService := mod.Client.ApiClient.ModulesApi requestBody := openapi.ModuleName{ Module: &openapi.SubjectNameSubject{ @@ -81,27 +103,33 @@ func (fn *ModService) Create(ctx context.Context, modName string) error { return pkg.ExtractError(err) } -func (fn *ModService) Delete(ctx context.Context, modName string) error { +func (mod *ModService) Delete(ctx context.Context, modName string) error { + + mod.injectHost(ctx) + mod.injectAPIToken() - if err := fn.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { + if err := mod.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { return err } - apiService := fn.Client.ApiClient.ModulesApi + apiService := mod.Client.ApiClient.ModulesApi _, err := apiService.DeleteModule(ctx, modName).Execute() return pkg.ExtractError(err) } -func (fn *ModService) Update(ctx context.Context, modName string, newName string) error { +func (mod *ModService) Update(ctx context.Context, modName string, newName string) error { - if err := fn.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { + mod.injectHost(ctx) + mod.injectAPIToken() + + if err := mod.InputValidatorHandler.ValidateName(modName, "mod"); err != nil { return err } - if err := fn.InputValidatorHandler.ValidateName(newName, "new mod"); err != nil { + if err := mod.InputValidatorHandler.ValidateName(newName, "new mod"); err != nil { return err } - apiService := fn.Client.ApiClient.ModulesApi + apiService := mod.Client.ApiClient.ModulesApi requestBody := openapi.ModuleName{ Module: &openapi.SubjectNameSubject{ Name: &newName, @@ -112,8 +140,12 @@ func (fn *ModService) Update(ctx context.Context, modName string, newName string return pkg.ExtractError(err) } -func (fn *ModService) List(ctx context.Context) (pkg.ModuleNameList, error) { - apiService := fn.Client.ApiClient.ModulesApi +func (mod *ModService) List(ctx context.Context) (pkg.ModuleNameList, error) { + + mod.injectHost(ctx) + mod.injectAPIToken() + + apiService := mod.Client.ApiClient.ModulesApi response, _, err := apiService.ListModules(ctx).Execute() if err != nil { return pkg.ModuleNameList{}, pkg.ExtractError(err) diff --git a/pkg/client/user_service.go b/pkg/client/user_service.go index 5c07e2a..9d7d603 100644 --- a/pkg/client/user_service.go +++ b/pkg/client/user_service.go @@ -32,7 +32,27 @@ type UserService struct { var _ UserHandler = &UserService{} +func (u *UserService) injectAdminToken() { + if u.Client != nil { + adminToken := u.Client.Config.AdminToken + apiConfig := u.Client.ApiClient.GetConfig() + apiConfig.DefaultHeader["Authorization"] = "Bearer " + adminToken + } +} + +func (u *UserService) injectHost(ctx context.Context) { + overrideHost, ok := ctx.Value(pkg.FLContextKey("api_host")).(string) + if ok && overrideHost != "" { + apiConfig := u.Client.ApiClient.GetConfig() + apiConfig.Host = overrideHost + } +} + func (u *UserService) Create(ctx context.Context, name string) (pkg.UserNameToken, error) { + + u.injectHost(ctx) + u.injectAdminToken() + apiService := u.Client.ApiClient.SubjectsApi requestBody := openapi.SubjectName{ @@ -52,6 +72,9 @@ func (u *UserService) Create(ctx context.Context, name string) (pkg.UserNameToke func (u *UserService) List(ctx context.Context) (pkg.UserNamesList, error) { + u.injectHost(ctx) + u.injectAdminToken() + apiService := u.Client.ApiClient.SubjectsApi res, _, err := apiService.ListSubjects(ctx).Execute() diff --git a/pkg/constants.go b/pkg/constants.go index 2920006..2986770 100644 --- a/pkg/constants.go +++ b/pkg/constants.go @@ -43,4 +43,6 @@ const ( DefaultTemplateRepository = "https://github.com/funlessdev/fl-templates.git" ConfigDir = ".fl" + ConfigFileName = "config" + ConfigKeys = "api_host,api_token,admin_token,secret_key_base" ) diff --git a/pkg/deploy/docker.go b/pkg/deploy/docker.go index 6dfe34e..976289c 100644 --- a/pkg/deploy/docker.go +++ b/pkg/deploy/docker.go @@ -16,46 +16,66 @@ package deploy import ( "bytes" + "context" + "fmt" "io" "os" "os/exec" "regexp" "strings" + + "github.com/funlessdev/fl-cli/pkg" ) type DockerShell interface { - ComposeUp(composeFilePath string) error - ComposeDown(composeFilePath string) error - ComposeList() ([]string, error) + ComposeUp(ctx context.Context, composeFilePath string) error + ComposeDown(ctx context.Context, composeFilePath string) error + ComposeList(ctx context.Context) ([]string, error) + LogTokens(ctx context.Context) error } type FLDockerShell struct{} -func (sh *FLDockerShell) ComposeUp(composeFilePath string) error { - return runShellCmd(os.Stdout, os.Stderr, "docker", "compose", "-f", composeFilePath, "up", "-d") +func (sh *FLDockerShell) ComposeUp(ctx context.Context, composeFilePath string) error { + return runShellCmd(ctx, os.Stdout, os.Stderr, "docker", "compose", "-f", composeFilePath, "up", "-d") } -func (sh *FLDockerShell) ComposeDown(composeFilePath string) error { - return runShellCmd(os.Stdout, os.Stderr, "docker", "compose", "-f", composeFilePath, "down") +func (sh *FLDockerShell) ComposeDown(ctx context.Context, composeFilePath string) error { + return runShellCmd(ctx, os.Stdout, os.Stderr, "docker", "compose", "-f", composeFilePath, "down") } -func (sh *FLDockerShell) ComposeList() ([]string, error) { +func (sh *FLDockerShell) ComposeList(ctx context.Context) ([]string, error) { var buf bytes.Buffer - err := runShellCmd(&buf, os.Stderr, "docker", "compose", "ls", "-q") + err := runShellCmd(ctx, &buf, os.Stderr, "docker", "compose", "ls", "-q") lines := strings.Split(buf.String(), "\n") return lines, err } -func runShellCmd(resultBuf io.Writer, errorBuf io.Writer, cmd string, args ...string) error { - exe, params := parseCmd(cmd, args...) +func (sh *FLDockerShell) LogTokens(ctx context.Context) error { + return runShellCmd(ctx, os.Stdout, os.Stderr, "docker", "exec", "fl-core-1", "cat", "/tmp/funless/tokens") +} + +func runShellCmd(ctx context.Context, resultBuf io.Writer, errorBuf io.Writer, cmd string, args ...string) error { + exe, params := parseCmd(ctx, cmd, args...) command := exec.Command(exe, params...) command.Stdout = resultBuf command.Stderr = errorBuf + ctxEnv, ok := ctx.Value(pkg.FLContextKey("env")).(map[string]string) + command.Env = os.Environ() + + if ok && ctxEnv != nil { + for k := range ctxEnv { + if ctxEnv[k] != "" { + command.Env = append(command.Env, fmt.Sprintf("%s=%s", k, ctxEnv[k])) + } + } + } + return command.Run() } -func parseCmd(cmd string, args ...string) (string, []string) { +func parseCmd(ctx context.Context, cmd string, args ...string) (string, []string) { re := regexp.MustCompile(`[\r\t\n\f ]+`) a := strings.Split(re.ReplaceAllString(cmd, " "), " ") diff --git a/pkg/deploy/docker_test.go b/pkg/deploy/docker_test.go index 5b86f4d..7f443df 100644 --- a/pkg/deploy/docker_test.go +++ b/pkg/deploy/docker_test.go @@ -16,6 +16,7 @@ package deploy import ( "bytes" + "context" "os" "testing" @@ -24,11 +25,13 @@ import ( func Test_parseCmd(t *testing.T) { t.Run("should split cmd from params in cmd string", func(t *testing.T) { - exe, _ := parseCmd("docker info") + testCtx := context.Background() + exe, _ := parseCmd(testCtx, "docker info") assert.Equal(t, "docker", exe) }) t.Run("should append arg in cmd string at the start of params array", func(t *testing.T) { - _, params := parseCmd("docker info", "hello") + testCtx := context.Background() + _, params := parseCmd(testCtx, "docker info", "hello") assert.Equal(t, []string{"info", "hello"}, params) }) } @@ -37,34 +40,42 @@ func Test_runShellCmd(t *testing.T) { t.Run("should return the command output in output buffer", func(t *testing.T) { var outBuf bytes.Buffer - err := runShellCmd(&outBuf, os.Stderr, "echo", "hello") + testCtx := context.Background() + + err := runShellCmd(testCtx, &outBuf, os.Stderr, "echo", "hello") assert.Nil(t, err) assert.Equal(t, "hello\n", outBuf.String()) }) t.Run("should return in error buffer all the contents of stderr", func(t *testing.T) { var errBuf bytes.Buffer - errSecond := runShellCmd(os.Stdout, &errBuf, "/bin/sh", "-c", "echo hello err 1>&2") + testCtx := context.Background() + + errSecond := runShellCmd(testCtx, os.Stdout, &errBuf, "/bin/sh", "-c", "echo hello err 1>&2") assert.Nil(t, errSecond) assert.Equal(t, "hello err\n", errBuf.String()) }) t.Run("should return an error in case of command fail", func(t *testing.T) { - err := runShellCmd(os.Stdout, os.Stderr, "exit", "1") + testCtx := context.Background() + + err := runShellCmd(testCtx, os.Stdout, os.Stderr, "exit", "1") assert.NotNil(t, err) }) t.Run("should return the content of StdOut and StdErr despite the command fail", func(t *testing.T) { var outBuf bytes.Buffer var errBuf bytes.Buffer - errFirst := runShellCmd(&outBuf, &errBuf, "echo", "hello out") + testCtx := context.Background() + + errFirst := runShellCmd(testCtx, &outBuf, &errBuf, "echo", "hello out") assert.Nil(t, errFirst) - errSecond := runShellCmd(os.Stdout, &errBuf, "/bin/sh", "-c", "echo hello err 1>&2") + errSecond := runShellCmd(testCtx, os.Stdout, &errBuf, "/bin/sh", "-c", "echo hello err 1>&2") assert.Nil(t, errSecond) - errThird := runShellCmd(&outBuf, &errBuf, "exit", "1") + errThird := runShellCmd(testCtx, &outBuf, &errBuf, "exit", "1") assert.NotNil(t, errThird) assert.Equal(t, "hello out\n", outBuf.String()) diff --git a/pkg/deploy/kubernetes_deployer.go b/pkg/deploy/kubernetes_deployer.go index db77b44..6a62e50 100644 --- a/pkg/deploy/kubernetes_deployer.go +++ b/pkg/deploy/kubernetes_deployer.go @@ -1,4 +1,4 @@ -// Copyright 2022 Giuseppe De Palma, Matteo Trentin +// Copyright 2023 Giuseppe De Palma, Matteo Trentin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,19 +15,27 @@ package deploy import ( + "bytes" "context" + "errors" "io" "net/http" "os" "path/filepath" + "time" + "github.com/funlessdev/fl-cli/pkg" apiAppsV1 "k8s.io/api/apps/v1" apiBatchV1 "k8s.io/api/batch/v1" apiCoreV1 "k8s.io/api/core/v1" apiRbacV1 "k8s.io/api/rbac/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/remotecommand" ) type KubernetesDeployer interface { @@ -43,15 +51,18 @@ type KubernetesDeployer interface { DeployPostgres(ctx context.Context) error DeployPostgresService(ctx context.Context) error StartInitPostgres(ctx context.Context) error + CreateCoreSecrets(ctx context.Context) error DeployCore(ctx context.Context) error DeployCoreService(ctx context.Context) error DeployWorker(ctx context.Context) error + + ExtractTokens(ctx context.Context, stdout *bytes.Buffer, stderr *bytes.Buffer) error } type FLKubernetesDeployer struct { kubernetesClientSet kubernetes.Interface - - namespace string + restConfig *rest.Config + namespace string } func getYAMLContent(url string) ([]byte, error) { @@ -90,6 +101,7 @@ func (k *FLKubernetesDeployer) WithConfig(config string) error { return err } + k.restConfig = kConfig k.kubernetesClientSet = clientSet return nil } @@ -284,6 +296,30 @@ func (k *FLKubernetesDeployer) StartInitPostgres(ctx context.Context) error { return err } +func (k *FLKubernetesDeployer) CreateCoreSecrets(ctx context.Context) error { + yml, err := getYAMLContent("https://raw.githubusercontent.com/funlessdev/fl-deploy/main/kind/core-secret-key-base.yml") + if err != nil { + return err + } + + typeMeta := v1.TypeMeta{Kind: "Secret", APIVersion: "v1"} + obj, err := ParseKubernetesYAML(yml, &apiCoreV1.Secret{TypeMeta: typeMeta}) + if err != nil { + return err + } + + secret := obj.(*apiCoreV1.Secret) + + overrideSecretKeyBase, ok := ctx.Value(pkg.FLContextKey("secret_key_base")).(string) + if ok && overrideSecretKeyBase != "" { + secret.Data["secret_key_base"] = []byte(overrideSecretKeyBase) + } + + _, err = k.kubernetesClientSet.CoreV1().Secrets(k.namespace).Create(ctx, secret, v1.CreateOptions{}) + + return err +} + func (k *FLKubernetesDeployer) DeployCore(ctx context.Context) error { yml, err := getYAMLContent("https://raw.githubusercontent.com/funlessdev/fl-deploy/main/kind/core.yml") if err != nil { @@ -340,3 +376,46 @@ func (k *FLKubernetesDeployer) DeployWorker(ctx context.Context) error { return err } + +func (k *FLKubernetesDeployer) ExtractTokens(ctx context.Context, stdout *bytes.Buffer, stderr *bytes.Buffer) error { + + corePods, err := k.kubernetesClientSet.CoreV1().Pods(k.namespace).List(ctx, v1.ListOptions{LabelSelector: "app=fl-core"}) + if err != nil { + return err + } + if len(corePods.Items) == 0 { + return errors.New("no pods matching app=fl-core") + } + + corePod := corePods.Items[0] + req := k.kubernetesClientSet.CoreV1().RESTClient().Post().Resource("pods").Name(corePod.Name).Namespace("fl").SubResource("exec") + options := &apiCoreV1.PodExecOptions{ + Command: []string{ + "sh", + "-c", + "cat /tmp/funless/tokens", + }, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + } + + req.VersionedParams(options, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(k.restConfig, "POST", req.URL()) + if err != nil { + return err + } + + err = wait.Poll(time.Second, time.Second*120, func() (done bool, err error) { + e := exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdin: nil, + Stdout: stdout, + Stderr: stderr, + }) + return e == nil, nil + }) + + return err +} diff --git a/pkg/types.go b/pkg/types.go index 8ed8bb0..0ffb579 100644 --- a/pkg/types.go +++ b/pkg/types.go @@ -50,6 +50,8 @@ type FLError struct { } `json:"errors"` } +type FLContextKey string + func ExtractError(err error) error { var e FLError openApiError, castOk := err.(*openapi.GenericOpenAPIError) diff --git a/test/integration/local_deploy_test.go b/test/integration/local_deploy_test.go index bbe6c7b..0c262fa 100644 --- a/test/integration/local_deploy_test.go +++ b/test/integration/local_deploy_test.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/client" "github.com/funlessdev/fl-cli/internal/command/admin" "github.com/funlessdev/fl-cli/pkg" + flClient "github.com/funlessdev/fl-cli/pkg/client" "github.com/funlessdev/fl-cli/pkg/deploy" "github.com/funlessdev/fl-cli/pkg/docker" "github.com/funlessdev/fl-cli/pkg/log" @@ -62,7 +63,7 @@ func TestAdminDevRun(t *testing.T) { ctx := context.Background() t.Run("should successfully deploy and remove funless when no errors occurr", func(t *testing.T) { - err := admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger) + err := admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger, flClient.Config{}) assert.NoError(t, err) assertContainer(t, flDocker, coreName) @@ -81,7 +82,7 @@ func TestAdminDevRun(t *testing.T) { t.Run("should successfully deploy without creating networks when they already exist", func(t *testing.T) { - err := admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger) + err := admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger, flClient.Config{}) assert.NoError(t, err) assertContainer(t, flDocker, coreName) @@ -104,7 +105,7 @@ func TestAdminDevRun(t *testing.T) { // _ = deployer.StartCore(ctx) assertContainer(t, flDocker, coreName) - err := admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger) + err := admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger, flClient.Config{}) assert.Error(t, err) err = admCmd.Deploy.Docker.Down.Run(ctx, dockerShell, logger) @@ -122,7 +123,7 @@ func TestAdminDevRun(t *testing.T) { os.RemoveAll(logFolder) // cleanup folder from previous test runs - err = admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger) + err = admCmd.Deploy.Docker.Up.Run(ctx, dockerShell, logger, flClient.Config{}) assert.NoError(t, err) assert.DirExists(t, logFolder) diff --git a/test/mocks/DockerShell.go b/test/mocks/DockerShell.go index 712cc7e..db3d135 100644 --- a/test/mocks/DockerShell.go +++ b/test/mocks/DockerShell.go @@ -1,4 +1,4 @@ -// Copyright 2022 Giuseppe De Palma, Matteo Trentin +// Copyright 2023 Giuseppe De Palma, Matteo Trentin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,24 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Code generated by mockery v2.20.2. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks -import mock "github.com/stretchr/testify/mock" +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) // DockerShell is an autogenerated mock type for the DockerShell type type DockerShell struct { mock.Mock } -// ComposeDown provides a mock function with given fields: composeFilePath -func (_m *DockerShell) ComposeDown(composeFilePath string) error { - ret := _m.Called(composeFilePath) +// ComposeDown provides a mock function with given fields: ctx, composeFilePath +func (_m *DockerShell) ComposeDown(ctx context.Context, composeFilePath string) error { + ret := _m.Called(ctx, composeFilePath) var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(composeFilePath) + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, composeFilePath) } else { r0 = ret.Error(0) } @@ -37,25 +41,25 @@ func (_m *DockerShell) ComposeDown(composeFilePath string) error { return r0 } -// ComposeList provides a mock function with given fields: -func (_m *DockerShell) ComposeList() ([]string, error) { - ret := _m.Called() +// ComposeList provides a mock function with given fields: ctx +func (_m *DockerShell) ComposeList(ctx context.Context) ([]string, error) { + ret := _m.Called(ctx) var r0 []string var r1 error - if rf, ok := ret.Get(0).(func() ([]string, error)); ok { - return rf() + if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { + return rf(ctx) } - if rf, ok := ret.Get(0).(func() []string); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -63,13 +67,27 @@ func (_m *DockerShell) ComposeList() ([]string, error) { return r0, r1 } -// ComposeUp provides a mock function with given fields: composeFilePath -func (_m *DockerShell) ComposeUp(composeFilePath string) error { - ret := _m.Called(composeFilePath) +// ComposeUp provides a mock function with given fields: ctx, composeFilePath +func (_m *DockerShell) ComposeUp(ctx context.Context, composeFilePath string) error { + ret := _m.Called(ctx, composeFilePath) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, composeFilePath) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// LogTokens provides a mock function with given fields: ctx +func (_m *DockerShell) LogTokens(ctx context.Context) error { + ret := _m.Called(ctx) var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(composeFilePath) + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) } else { r0 = ret.Error(0) } diff --git a/test/mocks/KubernetesDeployer.go b/test/mocks/KubernetesDeployer.go index f9c1213..44fd9ea 100644 --- a/test/mocks/KubernetesDeployer.go +++ b/test/mocks/KubernetesDeployer.go @@ -1,4 +1,4 @@ -// Copyright 2022 Giuseppe De Palma, Matteo Trentin +// Copyright 2023 Giuseppe De Palma, Matteo Trentin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Code generated by mockery v2.20.2. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks import ( + bytes "bytes" context "context" mock "github.com/stretchr/testify/mock" @@ -27,6 +28,20 @@ type KubernetesDeployer struct { mock.Mock } +// CreateCoreSecrets provides a mock function with given fields: ctx +func (_m *KubernetesDeployer) CreateCoreSecrets(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // CreateNamespace provides a mock function with given fields: ctx func (_m *KubernetesDeployer) CreateNamespace(ctx context.Context) error { ret := _m.Called(ctx) @@ -195,6 +210,20 @@ func (_m *KubernetesDeployer) DeployWorker(ctx context.Context) error { return r0 } +// ExtractTokens provides a mock function with given fields: ctx, stdout, stderr +func (_m *KubernetesDeployer) ExtractTokens(ctx context.Context, stdout *bytes.Buffer, stderr *bytes.Buffer) error { + ret := _m.Called(ctx, stdout, stderr) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *bytes.Buffer, *bytes.Buffer) error); ok { + r0 = rf(ctx, stdout, stderr) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // StartInitPostgres provides a mock function with given fields: ctx func (_m *KubernetesDeployer) StartInitPostgres(ctx context.Context) error { ret := _m.Called(ctx)