Skip to content

Commit

Permalink
Merge pull request #451 from kool-dev/2.0
Browse files Browse the repository at this point in the history
Kool CLI 2.0 - `kool cloud` and more
  • Loading branch information
fabriciojs authored Mar 17, 2023
2 parents 8a08010 + d956537 commit 9c66a1e
Show file tree
Hide file tree
Showing 53 changed files with 898 additions and 115 deletions.
31 changes: 31 additions & 0 deletions commands/cloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package commands

import "github.com/spf13/cobra"

func AddKoolCloud(root *cobra.Command) {
var (
cloudCmd = NewCloudCommand()
)

cloudCmd.AddCommand(NewDeployCommand(NewKoolDeploy()))
cloudCmd.AddCommand(NewDeployExecCommand(NewKoolDeployExec()))
cloudCmd.AddCommand(NewDeployDestroyCommand(NewKoolDeployDestroy()))
cloudCmd.AddCommand(NewDeployLogsCommand(NewKoolDeployLogs()))
cloudCmd.AddCommand(NewSetupCommand(NewKoolCloudSetup()))

root.AddCommand(cloudCmd)
}

// NewCloudCommand initializes new kool cloud command
func NewCloudCommand() (cloudCmd *cobra.Command) {
cloudCmd = &cobra.Command{
Use: "cloud COMMAND [flags]",
Short: "Interact with Kool Cloud and manage your deployments.",
Long: "The cloud subcommand encapsulates a set of APIs to interact with Kool Cloud and deploy, access and tail logs from your deployments.",
Example: `kool cloud deploy`,
// add cobra usage help content
DisableFlagsInUseLine: true,
}

return
}
16 changes: 2 additions & 14 deletions commands/deploy.go → commands/cloud_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"kool-dev/kool/core/builder"
"kool-dev/kool/core/environment"
"kool-dev/kool/services/cloud"
"kool-dev/kool/services/cloud/api"
"kool-dev/kool/services/tgz"
"os"
Expand Down Expand Up @@ -51,15 +52,6 @@ func NewKoolDeploy() *KoolDeploy {
}
}

func AddKoolDeploy(root *cobra.Command) {
deployCmd := NewDeployCommand(NewKoolDeploy())

root.AddCommand(deployCmd)
deployCmd.AddCommand(NewDeployExecCommand(NewKoolDeployExec()))
deployCmd.AddCommand(NewDeployDestroyCommand(NewKoolDeployDestroy()))
deployCmd.AddCommand(NewDeployLogsCommand(NewKoolDeployLogs()))
}

// Execute runs the deploy logic.
func (d *KoolDeploy) Execute(args []string) (err error) {
var (
Expand Down Expand Up @@ -260,11 +252,7 @@ func (d *KoolDeploy) handleDeployEnv(files []string) []string {
}

func (d *KoolDeploy) validate() (err error) {
var path = filepath.Join(d.env.Get("PWD"), koolDeployFile)

if _, err = os.Stat(path); os.IsNotExist(err) {
err = fmt.Errorf("could not find required file (%s) on current working directory", koolDeployFile)
}
err = cloud.ValidateKoolDeployFile(d.env.Get("PWD"), koolDeployFile)

return
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
189 changes: 189 additions & 0 deletions commands/cloud_setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package commands

import (
"fmt"
"kool-dev/kool/core/environment"
"kool-dev/kool/core/shell"
"kool-dev/kool/services/cloud"
"kool-dev/kool/services/compose"
"os"
"strings"

"github.com/spf13/cobra"
yaml3 "gopkg.in/yaml.v3"
)

// KoolCloudSetup holds handlers and functions for setting up deployment configuration
type KoolCloudSetup struct {
DefaultKoolService

promptSelect shell.PromptSelect
env environment.EnvStorage
}

// NewSetupCommand initializes new kool deploy Cobra command
func NewSetupCommand(setup *KoolCloudSetup) *cobra.Command {
return &cobra.Command{
Use: "setup",
Short: "Set up local configuration files for deployment",
RunE: DefaultCommandRunFunction(setup),
Args: cobra.NoArgs,

DisableFlagsInUseLine: true,
}
}

// NewKoolCloudSetup factories new KoolCloudSetup instance pointer
func NewKoolCloudSetup() *KoolCloudSetup {
return &KoolCloudSetup{
*newDefaultKoolService(),

shell.NewPromptSelect(),
environment.NewEnvStorage(),
}
}

// Execute runs the setup logic.
func (s *KoolCloudSetup) Execute(args []string) (err error) {
var (
composeConfig *compose.DockerComposeConfig
serviceName string

deployConfig *cloud.DeployConfig = &cloud.DeployConfig{
Services: make(map[string]*cloud.DeployConfigService),
}

postInstructions []func()
)

if !s.Shell().IsTerminal() {
err = fmt.Errorf("setup command is not available in non-interactive mode")
return
}

s.Shell().Warning("Warning: auto-setup is an experimental feature. Review all the generated configuration files before deploying.")
s.Shell().Info("Loading docker compose configuration...")

if composeConfig, err = compose.ParseConsolidatedDockerComposeConfig(s.env.Get("PWD")); err != nil {
return
}

s.Shell().Info("Docker compose configuration loaded. Starting interactive setup:")

for serviceName = range composeConfig.Services {
var (
answer string

composeService = composeConfig.Services[serviceName]
)

if answer, err = s.promptSelect.Ask(fmt.Sprintf("Do you want to deploy the service container '%s'?", serviceName), []string{"Yes", "No"}); err != nil {
return
}

if answer == "No" {
s.Shell().Warning(fmt.Sprintf("Not going to deploy service container '%s'", serviceName))
continue
}

s.Shell().Info(fmt.Sprintf("Setting up service container '%s' for deployment", serviceName))
deployConfig.Services[serviceName] = &cloud.DeployConfigService{}

// handle image/build config
if len(composeService.Volumes) == 0 && composeService.Build == nil {
// the simple-path - we have an image only and that is what we want to deploy
if image, isString := (*composeService.Image).(string); isString {
deployConfig.Services[serviceName].Image = new(string)
*deployConfig.Services[serviceName].Image = image
} else {
err = fmt.Errorf("unable to parse image configuration for service '%s'", serviceName)
return
}
} else {
// OK there's something for us to build... maybe the user is already building it?
// in case there's a build config, we'll use that
if composeService.Build != nil {
// if it's a string, that should be the build path...
if build, isString := (*composeService.Build).(string); isString {
if build != "." {
err = fmt.Errorf("service '%s' got a build dockerfile on path '%s'. Please move to the root folder/context to be able to deploy.", serviceName, build)
return
}
deployConfig.Services[serviceName].Build = new(string)
*deployConfig.Services[serviceName].Build = "Dockerfile"
} else if buildConfig, isMap := (*composeService.Build).(map[string]interface{}); isMap {
if ctx, exists := buildConfig["context"].(string); exists && ctx != "." {
err = fmt.Errorf("service '%s' got a build dockerfile on path '%s'. Please move to the root folder/context to be able to deploy.", serviceName, build)
return
}

if dockerfile, exists := buildConfig["dockerfile"].(string); exists {
deployConfig.Services[serviceName].Build = new(string)
*deployConfig.Services[serviceName].Build = dockerfile
} else {
err = fmt.Errorf("could not tell Dockerfile for service '%s'", serviceName)
return
}
}
} else {
// no build config, so we'll have to build it
deployConfig.Services[serviceName].Build = new(string)
*deployConfig.Services[serviceName].Build = "Dockerfile"

postInstructions = append(postInstructions, func() {
s.Shell().Info(fmt.Sprintf("⇒ Service '%s' needs to be built. Make sure to create the necessary Dockerfile.", serviceName))
})
}
}

// handle port/public config
ports := composeService.Ports
if len(ports) > 0 {
potentialPorts := []string{}
for i := range ports {
mappedPorts := strings.Split(ports[i], ":")

potentialPorts = append(potentialPorts, mappedPorts[len(mappedPorts)-1])
}

if len(potentialPorts) > 1 {
if answer, err = s.promptSelect.Ask("Which port do you want to make public?", potentialPorts); err != nil {
return
}
} else {
answer = potentialPorts[0]
}

deployConfig.Services[serviceName].Port = new(string)
*deployConfig.Services[serviceName].Port = answer

public := &cloud.DeployConfigPublicEntry{}
public.Port = new(string)
*public.Port = answer

deployConfig.Services[serviceName].Public = append(deployConfig.Services[serviceName].Public, public)
}
}

var yaml []byte
if yaml, err = yaml3.Marshal(deployConfig); err != nil {
return
}

if err = os.WriteFile(koolDeployFile, yaml, 0644); err != nil {
return
}

s.Shell().Println("")

for _, instruction := range postInstructions {
instruction()
}

s.Shell().Println("")
s.Shell().Println("")
s.Shell().Success("Setup completed. Please review the generated configuration file before deploying.")
s.Shell().Println("")

return
}
48 changes: 43 additions & 5 deletions commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"kool-dev/kool/core/environment"
"kool-dev/kool/core/presets"
"kool-dev/kool/core/shell"
"os"
"path"
"path/filepath"
Expand All @@ -22,8 +23,7 @@ type KoolCreate struct {

func AddKoolCreate(root *cobra.Command) {
var (
create = NewKoolCreate()
createCmd = NewCreateCommand(create)
createCmd = NewCreateCommand(NewKoolCreate())
)

root.AddCommand(createCmd)
Expand All @@ -41,10 +41,48 @@ func NewKoolCreate() *KoolCreate {
// Execute runs the create logic with incoming arguments.
func (c *KoolCreate) Execute(args []string) (err error) {
var (
preset = args[0]
createDirectory = args[1]
createDirectory, preset string
)

if len(args) == 2 {
preset = args[0]
createDirectory = args[1]
} else if len(args) == 1 {
err = fmt.Errorf("bad number of arguments - either specify both preset and directory or none")
return
} else {
if preset, err = NewKoolPreset().getPreset(args); err != nil {
return
}

for {
if createDirectory, err = shell.NewPromptInput().Input("New folder name:", fmt.Sprintf("my-kool-%s-project", preset)); err != nil {
return
}

if createDirectory == "" {
c.Shell().Error(fmt.Errorf("Please enter a valid folder name"))
continue
} else if _, err = os.Stat(createDirectory); !os.IsNotExist(err) {
c.Shell().Error(fmt.Errorf("Folder %s already exists.", createDirectory))
continue
} else {
if err = os.MkdirAll(filepath.Join(os.TempDir(), createDirectory), 0755); err != nil {
c.Shell().Error(fmt.Errorf("Please enter a valid folder name"))
continue
} else {
// ok we created, let's just have it removed if we fail
defer func() {
_ = os.RemoveAll(filepath.Join(os.TempDir(), createDirectory))
}()
}
}

// if no error, we got our directory
break
}
}

// sets env variable CREATE_DIRECTORY so preset can use it
c.env.Set("CREATE_DIRECTORY", createDirectory)

Expand Down Expand Up @@ -90,7 +128,7 @@ func NewCreateCommand(create *KoolCreate) (createCmd *cobra.Command) {
Use: "create PRESET FOLDER",
Short: "Create a new project using a preset",
Long: "Create a new project using the specified PRESET in a directory named FOLDER.",
Args: cobra.ExactArgs(2),
Args: cobra.MaximumNArgs(2),
RunE: DefaultCommandRunFunction(create),

DisableFlagsInUseLine: true,
Expand Down
4 changes: 1 addition & 3 deletions commands/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

// KoolDockerFlags holds the flags for the docker command
type KoolDockerFlags struct {
DisableTty bool
EnvVariables []string
Volumes []string
Publish []string
Expand Down Expand Up @@ -40,7 +39,7 @@ func AddKoolDocker(root *cobra.Command) {
func NewKoolDocker() *KoolDocker {
return &KoolDocker{
*newDefaultKoolService(),
&KoolDockerFlags{false, []string{}, []string{}, []string{}, []string{}},
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}},
environment.NewEnvStorage(),
builder.NewCommand("docker", "run", "--init", "--rm", "-w", "/app", "-i"),
}
Expand Down Expand Up @@ -104,7 +103,6 @@ the [COMMAND] to provide optional arguments required by the COMMAND.`,
DisableFlagsInUseLine: true,
}

cmd.Flags().BoolVarP(&docker.Flags.DisableTty, "disable-tty", "T", false, "Deprecated - no effect.")
cmd.Flags().StringArrayVarP(&docker.Flags.EnvVariables, "env", "e", []string{}, "Environment variables.")
cmd.Flags().StringArrayVarP(&docker.Flags.Volumes, "volume", "v", []string{}, "Bind mount a volume.")
cmd.Flags().StringArrayVarP(&docker.Flags.Publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
Expand Down
8 changes: 2 additions & 6 deletions commands/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func newFakeKoolDocker() *KoolDocker {
return &KoolDocker{
*(newDefaultKoolService().Fake()),
&KoolDockerFlags{false, []string{}, []string{}, []string{}, []string{}},
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}},
environment.NewFakeEnvStorage(),
&builder.FakeCommand{MockCmd: "docker"},
}
Expand All @@ -23,7 +23,7 @@ func newFakeKoolDocker() *KoolDocker {
func newFailedFakeKoolDocker() *KoolDocker {
return &KoolDocker{
*(newDefaultKoolService().Fake()),
&KoolDockerFlags{false, []string{}, []string{}, []string{}, []string{}},
&KoolDockerFlags{[]string{}, []string{}, []string{}, []string{}},
environment.NewFakeEnvStorage(),
&builder.FakeCommand{MockCmd: "docker", MockInteractiveError: errors.New("error docker")},
}
Expand All @@ -39,10 +39,6 @@ func TestNewKoolDocker(t *testing.T) {
if k.Flags == nil {
t.Errorf("Flags not initialized on default KoolDocker instance")
} else {
if k.Flags.DisableTty {
t.Errorf("bad default value for DisableTty flag on default KoolDocker instance")
}

if len(k.Flags.EnvVariables) > 0 {
t.Errorf("bad default value for EnvVariables flag on default KoolDocker instance")
}
Expand Down
Loading

0 comments on commit 9c66a1e

Please sign in to comment.