diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f62c713..7a14ba3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,13 +7,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # pin@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@v3 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # pin@v5 with: - go-version: 1.18 + go-version: 1.22 - name: Vet run: go vet ./... release: @@ -22,22 +22,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # pin@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@v3 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # pin@v5 with: - go-version: 1.18 + go-version: 1.22 - name: Generate Token id: generate-token - uses: tibdex/github-app-token@021a2405c7f990db57f5eae5397423dcc554159c # pin@v1 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # pin@v2 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.PRIVATE_KEY }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@b508e2e3ef3b19d4e4146d4f8fb3ba9db644a757 # pin@v3 + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # pin@v6 with: - args: release --rm-dist + args: release --clean env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} diff --git a/.goreleaser.yml b/.goreleaser.yml index e4db454..d2d35f2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,4 @@ +version: 2 before: hooks: - go mod download @@ -21,16 +22,16 @@ archives: files: - none* brews: - - tap: + - repository: owner: lade-io name: homebrew-tap - folder: Formula + directory: Formula homepage: https://github.com/lade-io/lade description: Developer tool to manage your apps checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" changelog: sort: asc filters: diff --git a/README.md b/README.md index a09f2b1..63f0e8a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Commands: addons Manage addons apps Manage apps deploy Deploy an app + disks Manage disks domains Manage domains env Manage app environment help Help about any command diff --git a/cmd/apps.go b/cmd/apps.go index bca5ade..a3c4c94 100644 --- a/cmd/apps.go +++ b/cmd/apps.go @@ -167,8 +167,16 @@ func getAppName() string { return filepath.Base(cwd) } +func getDiskPlan(client *lade.Client) string { + plan, err := client.Plan.Default("disk") + if err != nil { + return "" + } + return plan.ID +} + func getPlan(client *lade.Client) string { - plan, err := client.Plan.Default() + plan, err := client.Plan.Default("") if err != nil { return "" } diff --git a/cmd/disks.go b/cmd/disks.go new file mode 100644 index 0000000..a3d45dc --- /dev/null +++ b/cmd/disks.go @@ -0,0 +1,212 @@ +package cmd + +import ( + "strconv" + + "github.com/AlecAivazis/survey/v2" + "github.com/dustin/go-humanize" + "github.com/lade-io/go-lade" + "github.com/rodaine/table" + "github.com/spf13/cobra" +) + +var disksCmd = &cobra.Command{ + Use: "disks", + Short: "Manage disks", +} + +var disksAddCmd = func() *cobra.Command { + var appName string + opts := &lade.DiskCreateOpts{} + cmd := &cobra.Command{ + Use: "add ", + Short: "Add a disk to an app", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + if len(args) > 0 { + opts.Name = args[0] + } + return disksAddRun(client, opts, appName) + }, + } + cmd.Flags().StringVarP(&appName, "app", "a", "", "App Name") + cmd.Flags().StringVar(&opts.Path, "path", "", "Path") + cmd.Flags().StringVarP(&opts.PlanID, "plan", "p", "", "Plan") + return cmd +}() + +var disksListCmd = func() *cobra.Command { + var appName string + cmd := &cobra.Command{ + Use: "list", + Short: "List disks of an app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + return disksListRun(client, appName) + }, + } + cmd.Flags().StringVarP(&appName, "app", "a", "", "App Name") + return cmd +}() + +var disksPlansCmd = &cobra.Command{ + Use: "plans", + Short: "List available plans", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + return disksPlansRun(client) + }, +} + +var disksRemoveCmd = func() *cobra.Command { + var appName string + cmd := &cobra.Command{ + Use: "remove ", + Short: "Remove a disk from an app", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + var diskName string + if len(args) > 0 { + diskName = args[0] + } + return disksRemoveRun(client, appName, diskName) + }, + } + cmd.Flags().StringVarP(&appName, "app", "a", "", "App Name") + return cmd +}() + +var disksUpdateCmd = func() *cobra.Command { + var appName string + opts := &lade.DiskUpdateOpts{} + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a disk of an app", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + var diskName string + if len(args) > 0 { + diskName = args[0] + } + return disksUpdateRun(client, opts, appName, diskName) + }, + } + cmd.Flags().StringVarP(&appName, "app", "a", "", "App Name") + cmd.Flags().StringVarP(&opts.PlanID, "plan", "p", "", "Plan") + return cmd +}() + +func init() { + disksCmd.AddCommand(disksAddCmd) + disksCmd.AddCommand(disksListCmd) + disksCmd.AddCommand(disksPlansCmd) + disksCmd.AddCommand(disksRemoveCmd) + disksCmd.AddCommand(disksUpdateCmd) +} + +func disksAddRun(client *lade.Client, opts *lade.DiskCreateOpts, appName string) error { + if err := askSelect("App Name:", getAppName, client, getAppOptions, &appName); err != nil { + return err + } + if err := askInput("Disk Name:", appName, &opts.Name, validateDiskName(client, appName)); err != nil { + return err + } + if err := askSelect("Plan:", getDiskPlan, client, getDiskPlanOptions(""), &opts.PlanID); err != nil { + return err + } + if err := askInput("Path:", "/data", &opts.Path, validatePath); err != nil { + return err + } + _, err := client.Disk.Create(appName, opts) + return err +} + +func disksListRun(client *lade.Client, appName string) error { + if err := askSelect("App Name:", getAppName, client, getAppOptions, &appName); err != nil { + return err + } + disks, err := client.Disk.List(appName) + if err != nil { + return err + } + t := table.New("NAME", "PLAN", "PATH", "CREATED") + for _, disk := range disks { + t.AddRow(disk.Name, disk.PlanID, disk.Path, humanize.Time(disk.CreatedAt)) + } + t.Print() + return nil +} + +func disksPlansRun(client *lade.Client) error { + plans, err := client.Plan.List("disk") + if err != nil { + return err + } + t := table.New("ID", "DISK", "PRICE HOURLY", "PRICE MONTHLY") + for _, plan := range plans { + priceHourly := printPrice(plan.PriceHourly, -1) + priceMonthly := printPrice(plan.PriceMonthly, 2) + t.AddRow(plan.ID, plan.Disk, priceHourly, priceMonthly) + } + t.Print() + return nil +} + +func disksRemoveRun(client *lade.Client, appName, diskName string) error { + if err := askSelect("App Name:", getAppName, client, getAppOptions, &appName); err != nil { + return err + } + if err := askSelect("Disk Name:", "", client, getDiskOptions(appName), &diskName); err != nil { + return err + } + disk, err := client.Disk.Get(appName, diskName) + if err != nil { + return err + } + prompt := &survey.Confirm{ + Message: "Do you really want to delete " + disk.Name + "?", + } + confirm := false + survey.AskOne(prompt, &confirm, nil) + if confirm { + err = client.Disk.Delete(disk) + } + return err +} + +func disksUpdateRun(client *lade.Client, opts *lade.DiskUpdateOpts, appName, diskName string) error { + if err := askSelect("App Name:", getAppName, client, getAppOptions, &appName); err != nil { + return err + } + if err := askSelect("Disk Name:", "", client, getDiskOptions(appName), &diskName); err != nil { + return err + } + disk, err := client.Disk.Get(appName, diskName) + if err != nil { + return err + } + if err = askSelect("Plan:", disk.PlanID, client, getDiskPlanOptions(disk.PlanID), &opts.PlanID); err != nil { + return err + } + _, err = client.Disk.Update(strconv.Itoa(disk.AppID), strconv.Itoa(disk.ID), opts) + return err +} diff --git a/cmd/plans.go b/cmd/plans.go index 04bef7e..b1052e0 100644 --- a/cmd/plans.go +++ b/cmd/plans.go @@ -20,7 +20,7 @@ var plansCmd = &cobra.Command{ } func plansRun(client *lade.Client) error { - plans, err := client.Plan.List() + plans, err := client.Plan.List("") if err != nil { return err } diff --git a/cmd/prompt.go b/cmd/prompt.go index 5079f33..1d47183 100644 --- a/cmd/prompt.go +++ b/cmd/prompt.go @@ -29,6 +29,7 @@ import ( var ( validEnvName = regexp.MustCompile(`^[A-Z0-9-_]+$`) validName = regexp.MustCompile(`^[a-z][a-z0-9-_]*$`) + validPath = regexp.MustCompile(`^(/[a-zA-Z0-9-_]+)+$`) ) type optionsFunc func(*lade.Client) (*orderedmap.OrderedMap, error) @@ -164,6 +165,41 @@ func getAddonOptions(client *lade.Client) (*orderedmap.OrderedMap, error) { return options, nil } +func getDiskOptions(appName string) optionsFunc { + return func(client *lade.Client) (*orderedmap.OrderedMap, error) { + disks, err := client.Disk.List(appName) + if err != nil { + return nil, err + } + if len(disks) == 0 { + return nil, errors.New("There are no disks available") + } + options := orderedmap.New() + for _, disk := range disks { + options.Set(disk.Name, disk.Name) + } + options.SortKeys(sort.Strings) + return options, nil + } +} + +func getDiskPlanOptions(id string) optionsFunc { + return func(client *lade.Client) (*orderedmap.OrderedMap, error) { + plans, err := client.Plan.User(id, "disk") + if err != nil { + return nil, err + } + if len(plans) == 0 { + return nil, errors.New("There are no plans available") + } + options := orderedmap.New() + for _, plan := range plans { + options.Set(plan.ID, plan.ID) + } + return options, nil + } +} + func getDomainOptions(appName string) optionsFunc { return func(client *lade.Client) (*orderedmap.OrderedMap, error) { domains, err := client.Domain.List(appName) @@ -200,7 +236,7 @@ func getKeyOptions(appName string) optionsFunc { func getPlanOptions(id string) optionsFunc { return func(client *lade.Client) (*orderedmap.OrderedMap, error) { - plans, err := client.Plan.User(id) + plans, err := client.Plan.User(id, "") if err != nil { return nil, err } @@ -399,6 +435,12 @@ func validateAppName(client *lade.Client) survey.Validator { })) } +func validateDiskName(client *lade.Client, appName string) survey.Validator { + return survey.ComposeValidators(survey.Required, validateUniqueName(func(name string) error { + return client.Disk.Head(appName, name) + })) +} + func validateDomainName(client *lade.Client, appName string) survey.Validator { return survey.ComposeValidators(survey.Required, validateUniqueName(func(name string) error { return client.Domain.Head(appName, name) @@ -435,6 +477,13 @@ func validateName(val interface{}) error { return nil } +func validatePath(val interface{}) error { + if !validPath.MatchString(val.(string)) { + return errors.New("Path must be valid absolute directory") + } + return nil +} + func validateUniqueName(fn func(string) error) survey.Validator { return func(val interface{}) error { err := fn(val.(string)) diff --git a/cmd/root.go b/cmd/root.go index 18e61ac..efb959c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -37,6 +37,7 @@ func init() { RootCmd.AddCommand(addonsCmd) RootCmd.AddCommand(appsCmd) RootCmd.AddCommand(deployCmd) + RootCmd.AddCommand(disksCmd) RootCmd.AddCommand(domainsCmd) RootCmd.AddCommand(envCmd) RootCmd.AddCommand(loginCmd) diff --git a/cmd/usage.go b/cmd/usage.go index 6405e42..fe3094e 100644 --- a/cmd/usage.go +++ b/cmd/usage.go @@ -16,10 +16,16 @@ Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} Examples: -{{.Example}}{{end}}{{if .HasAvailableSubCommands}} +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} -Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} +Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} Options: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} diff --git a/go.mod b/go.mod index e530e04..db50933 100644 --- a/go.mod +++ b/go.mod @@ -8,14 +8,14 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/iancoleman/orderedmap v0.0.0-20180606015914-fec04b9a4f6d github.com/jinzhu/configor v1.1.1 - github.com/lade-io/go-lade v0.1.8 + github.com/lade-io/go-lade v0.1.9 github.com/mattn/go-colorable v0.1.12 github.com/mattn/go-isatty v0.0.14 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 github.com/rodaine/table v1.0.1 - github.com/spf13/cobra v1.5.0 + github.com/spf13/cobra v1.7.0 golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 gopkg.in/yaml.v3 v3.0.1 ) @@ -29,7 +29,7 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.13.1 // indirect github.com/klauspost/pgzip v1.2.5 // indirect diff --git a/go.sum b/go.sum index 15ba891..4f66e15 100644 --- a/go.sum +++ b/go.sum @@ -401,8 +401,9 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jinzhu/configor v1.1.1 h1:gntDP+ffGhs7aJ0u8JvjCDts2OsxsI7bnz3q+jC+hSY= github.com/jinzhu/configor v1.1.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= @@ -444,16 +445,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lade-io/go-lade v0.1.4 h1:wrhPA8keh4u1hqNF3GUAPo2RbMmF1q2EUisPAK+qhkU= -github.com/lade-io/go-lade v0.1.4/go.mod h1:kQDkodhmNS1E7FMCVSGmSqvQPgwyNk47tS42JwfrszA= -github.com/lade-io/go-lade v0.1.5 h1:Am5fEt3SaURMaGzGqX82NqkiH8yG1hEu0TcE4+m1Nso= -github.com/lade-io/go-lade v0.1.5/go.mod h1:kQDkodhmNS1E7FMCVSGmSqvQPgwyNk47tS42JwfrszA= -github.com/lade-io/go-lade v0.1.6 h1:WA+4Br3H5W6g0yRe2ep0O0QDSTSVQ9qPm01JE9ZO5BY= -github.com/lade-io/go-lade v0.1.6/go.mod h1:kQDkodhmNS1E7FMCVSGmSqvQPgwyNk47tS42JwfrszA= -github.com/lade-io/go-lade v0.1.7 h1:SobPauUkN+n9SZ6RRGS8uNFt6PaviFxzeT1IN65Eui4= -github.com/lade-io/go-lade v0.1.7/go.mod h1:kQDkodhmNS1E7FMCVSGmSqvQPgwyNk47tS42JwfrszA= -github.com/lade-io/go-lade v0.1.8 h1:AaYZXy5epefN3daQogyDJoy1CiIULtYF31xBFb3Lexg= -github.com/lade-io/go-lade v0.1.8/go.mod h1:kQDkodhmNS1E7FMCVSGmSqvQPgwyNk47tS42JwfrszA= +github.com/lade-io/go-lade v0.1.9 h1:cLbAveuIwJDZbS/kiziNQ6TJDT/UOUaHhq0X0akAdqo= +github.com/lade-io/go-lade v0.1.9/go.mod h1:kQDkodhmNS1E7FMCVSGmSqvQPgwyNk47tS42JwfrszA= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -622,8 +615,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=