diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go index b85d3a1..533b57c 100644 --- a/internal/cmd/deploy/deploy.go +++ b/internal/cmd/deploy/deploy.go @@ -33,7 +33,6 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "deploy", Short: "Deploy local project to Zeabur with one command", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runDeploy(f, opts) }, diff --git a/internal/cmd/deployment/get/get.go b/internal/cmd/deployment/get/get.go index 68f8609..c12c285 100644 --- a/internal/cmd/deployment/get/get.go +++ b/internal/cmd/deployment/get/get.go @@ -24,20 +24,17 @@ func NewCmdGet(f *cmdutil.Factory) *cobra.Command { opts := &Options{} cmd := &cobra.Command{ - Use: "get", - Short: "Get deployment, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), + Use: "get", + Short: "Get deployment, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", RunE: func(cmd *cobra.Command, args []string) error { return runGet(f, opts) }, } - zctx := f.Config.GetContext() - cmd.Flags().StringVar(&opts.deploymentID, "deployment-id", "", "Deployment ID") - cmd.Flags().StringVar(&opts.serviceID, "service-id", zctx.GetService().GetID(), "Service ID") - cmd.Flags().StringVar(&opts.serviceName, "service-name", zctx.GetService().GetName(), "Service Name") - cmd.Flags().StringVar(&opts.environmentID, "env-id", zctx.GetEnvironment().GetID(), "Environment ID") + cmd.Flags().StringVar(&opts.serviceID, "service-id", "", "Service ID") + cmd.Flags().StringVar(&opts.serviceName, "service-name", "", "Service Name") + cmd.Flags().StringVar(&opts.environmentID, "env-id", "", "Environment ID") return cmd } @@ -69,39 +66,39 @@ func runGetInteractive(f *cmdutil.Factory, opts *Options) error { } func runGetNonInteractive(f *cmdutil.Factory, opts *Options) (err error) { - if opts.deploymentID == "" && opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, projectID) - if resolveErr != nil { - return resolveErr + // If deployment ID is provided, just use it directly + if opts.deploymentID != "" { + deployment, err := getDeploymentByID(f, opts.deploymentID) + if err != nil { + return err } - opts.environmentID = envID + f.Printer.Table(deployment.Header(), deployment.Rows()) + return nil } - if err = paramCheck(opts); err != nil { - return err + // Resolve service ID from name + if opts.serviceID == "" && opts.serviceName != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName) + if err != nil { + return fmt.Errorf("failed to get service: %w", err) + } + opts.serviceID = service.ID } - var deployment *model.Deployment + if opts.serviceID == "" { + return errors.New("--deployment-id or --service-id/--service-name is required") + } - // If deployment id is provided, get deployment by deployment id - if opts.deploymentID != "" { - deployment, err = getDeploymentByID(f, opts.deploymentID) - } else { - // or, get deployment by service id and environment id - - // If service id is not provided, get service id by service name - if opts.serviceID == "" { - var service *model.Service - if service, err = util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName); err != nil { - return fmt.Errorf("failed to get service: %w", err) - } else { - opts.serviceID = service.ID - } + // Resolve environment from service's project + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.serviceID) + if err != nil { + return err } - deployment, err = getDeploymentByServiceAndEnvironment(f, opts.serviceID, opts.environmentID) + opts.environmentID = envID } + deployment, err := getDeploymentByServiceAndEnvironment(f, opts.serviceID, opts.environmentID) if err != nil { return err } @@ -132,19 +129,3 @@ func getDeploymentByServiceAndEnvironment(f *cmdutil.Factory, serviceID, environ return deployment, nil } - -func paramCheck(opts *Options) error { - if opts.deploymentID != "" { - return nil - } - - if opts.serviceID == "" && opts.serviceName == "" { - return errors.New("when deployment-id is not specified, service-id or service-name is required") - } - - if opts.environmentID == "" { - return errors.New("when deployment-id is not specified, env-id is required") - } - - return nil -} diff --git a/internal/cmd/deployment/list/list.go b/internal/cmd/deployment/list/list.go index 3c34c69..81b1d0a 100644 --- a/internal/cmd/deployment/list/list.go +++ b/internal/cmd/deployment/list/list.go @@ -12,7 +12,6 @@ import ( ) type Options struct { - // todo: support service name serviceID string serviceName string environmentID string @@ -25,17 +24,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { Use: "list", Short: "List deployments", Aliases: []string{"ls"}, - PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runList(f, opts) }, } - zctx := f.Config.GetContext() - - cmd.Flags().StringVar(&opts.serviceID, "service-id", zctx.GetService().GetID(), "Service ID") - cmd.Flags().StringVar(&opts.serviceName, "service-name", zctx.GetService().GetName(), "Service Name") - cmd.Flags().StringVar(&opts.environmentID, "env-id", zctx.GetEnvironment().GetID(), "Environment ID") + cmd.Flags().StringVar(&opts.serviceID, "service-id", "", "Service ID") + cmd.Flags().StringVar(&opts.serviceName, "service-name", "", "Service Name") + cmd.Flags().StringVar(&opts.environmentID, "env-id", "", "Environment ID") return cmd } @@ -65,26 +61,26 @@ func runListInteractive(f *cmdutil.Factory, opts *Options) error { } func runListNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + // Resolve service ID from name + if opts.serviceID == "" && opts.serviceName != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName) if err != nil { - return err + return fmt.Errorf("failed to get service: %w", err) } - opts.environmentID = envID + opts.serviceID = service.ID } - if err := paramCheck(opts); err != nil { - return err + if opts.serviceID == "" { + return errors.New("--service-id or --service-name is required") } - // If service id is not provided, get service id by service name - if opts.serviceID == "" { - if service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName); err != nil { - return fmt.Errorf("failed to get service: %w", err) - } else { - opts.serviceID = service.ID + // Resolve environment from service's project + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.serviceID) + if err != nil { + return err } + opts.environmentID = envID } deployments, err := f.ApiClient.ListAllDeployments(context.Background(), opts.serviceID, opts.environmentID) @@ -101,15 +97,3 @@ func runListNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.serviceID == "" && opts.serviceName == "" { - return errors.New("service-id or service-name is required") - } - - if opts.environmentID == "" { - return errors.New("environment is required") - } - - return nil -} diff --git a/internal/cmd/deployment/log/log.go b/internal/cmd/deployment/log/log.go index d241071..456cffd 100644 --- a/internal/cmd/deployment/log/log.go +++ b/internal/cmd/deployment/log/log.go @@ -33,21 +33,18 @@ func NewCmdLog(f *cmdutil.Factory) *cobra.Command { opts := &Options{} cmd := &cobra.Command{ - Use: "log", - Short: "Get deployment logs, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), + Use: "log", + Short: "Get deployment logs, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", RunE: func(cmd *cobra.Command, args []string) error { return runLog(f, opts) }, } - zctx := f.Config.GetContext() - - cmd.Flags().StringVar(&opts.projectID, "project-id", zctx.GetProject().GetID(), "Project ID") + cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID") cmd.Flags().StringVar(&opts.deploymentID, "deployment-id", "", "Deployment ID") - cmd.Flags().StringVar(&opts.serviceID, "service-id", zctx.GetService().GetID(), "Service ID") - cmd.Flags().StringVar(&opts.serviceName, "service-name", zctx.GetService().GetName(), "Service Name") - cmd.Flags().StringVar(&opts.environmentID, "env-id", zctx.GetEnvironment().GetID(), "Environment ID") + cmd.Flags().StringVar(&opts.serviceID, "service-id", "", "Service ID") + cmd.Flags().StringVar(&opts.serviceName, "service-name", "", "Service Name") + cmd.Flags().StringVar(&opts.environmentID, "env-id", "", "Environment ID") cmd.Flags().StringVarP(&opts.logType, "type", "t", logTypeRuntime, "Log type, runtime or build") cmd.Flags().BoolVarP(&opts.watch, "watch", "w", false, "Watch logs") @@ -63,13 +60,8 @@ func runLog(f *cmdutil.Factory, opts *Options) error { } func runLogInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - - if opts.projectID == "" { - opts.projectID = zctx.GetProject().GetID() - } - if opts.deploymentID == "" { + zctx := f.Config.GetContext() _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ ProjectCtx: zctx, ServiceID: &opts.serviceID, @@ -95,8 +87,7 @@ func runLogNonInteractive(f *cmdutil.Factory, opts *Options) (err error) { opts.serviceID = service.ID } - // When serviceID is available, always resolve projectID and environmentID from the service - // instead of relying on context (which may point to a different project). + // When serviceID is available, resolve projectID and environmentID from the service if opts.serviceID != "" { service, err := f.ApiClient.GetService(context.Background(), opts.serviceID, "", "", "") if err != nil { @@ -105,24 +96,13 @@ func runLogNonInteractive(f *cmdutil.Factory, opts *Options) (err error) { if service.Project != nil { opts.projectID = service.Project.ID } - envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, opts.projectID) - if resolveErr != nil { - return resolveErr - } - opts.environmentID = envID - } - - // Fallback: resolve environmentID from context project if still empty - if opts.deploymentID == "" && opts.environmentID == "" { - projectID := opts.projectID - if projectID == "" { - projectID = f.Config.GetContext().GetProject().GetID() - } - envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, projectID) - if resolveErr != nil { - return resolveErr + if opts.environmentID == "" { + envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, opts.projectID) + if resolveErr != nil { + return resolveErr + } + opts.environmentID = envID } - opts.environmentID = envID } if err = paramCheck(opts); err != nil { diff --git a/internal/cmd/domain/create/create.go b/internal/cmd/domain/create/create.go index f858493..f6fdfad 100644 --- a/internal/cmd/domain/create/create.go +++ b/internal/cmd/domain/create/create.go @@ -23,17 +23,11 @@ type Options struct { func NewCmdCreateDomain(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "create", Short: "create a domain", Long: `Create a domain for a service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runCreateDomain(f, opts) }, @@ -93,7 +87,17 @@ func runCreateDomainInteractive(f *cmdutil.Factory, opts *Options) error { opts.domainName = domainInput } - project, err := f.ApiClient.GetProject(context.Background(), zctx.GetProject().GetID(), "", "") + // Get project from the service to check domain availability + service, err := f.ApiClient.GetService(context.Background(), opts.id, "", "", "") + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + projectID := "" + if service.Project != nil { + projectID = service.Project.ID + } + + project, err := f.ApiClient.GetProject(context.Background(), projectID, "", "") if err != nil { return err } @@ -146,16 +150,24 @@ func runCreateDomainInteractive(f *cmdutil.Factory, opts *Options) error { } func runCreateDomainNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/domain/delete/delete.go b/internal/cmd/domain/delete/delete.go index b234b0a..c6f4e92 100644 --- a/internal/cmd/domain/delete/delete.go +++ b/internal/cmd/domain/delete/delete.go @@ -22,18 +22,12 @@ type Options struct { func NewCmdDeleteDomain(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "delete", Short: "Delete domain", Long: `Delete domain of a service`, Aliases: []string{"del"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDeleteDomain(f, opts) }, @@ -112,16 +106,20 @@ func runDeleteDomainInteractive(f *cmdutil.Factory, opts *Options) error { } func runDeleteDomainNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.environmentID == "" && opts.id != "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/domain/list/list.go b/internal/cmd/domain/list/list.go index d087280..673406f 100644 --- a/internal/cmd/domain/list/list.go +++ b/internal/cmd/domain/list/list.go @@ -20,7 +20,6 @@ type Options struct { func NewCmdListDomains(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "list", @@ -28,11 +27,6 @@ func NewCmdListDomains(f *cmdutil.Factory) *cobra.Command { Long: `List domains of a service`, Args: cobra.NoArgs, Aliases: []string{"ls"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runListDomains(f, opts) }, @@ -69,16 +63,24 @@ func runListDomainsInteractive(f *cmdutil.Factory, opts *Options) error { } func runListDomainsNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/service/delete/delete.go b/internal/cmd/service/delete/delete.go index 5b919da..bb9aaf1 100644 --- a/internal/cmd/service/delete/delete.go +++ b/internal/cmd/service/delete/delete.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "delete", Short: "Delete a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDelete(f, opts) }, @@ -67,26 +61,24 @@ func runDeleteInteractive(f *cmdutil.Factory, opts *Options) error { } func runDeleteNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -118,15 +110,3 @@ func runDeleteNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/deploy/deploy.go b/internal/cmd/service/deploy/deploy.go index 7df7944..00e1fb4 100644 --- a/internal/cmd/service/deploy/deploy.go +++ b/internal/cmd/service/deploy/deploy.go @@ -8,7 +8,6 @@ import ( "github.com/briandowns/spinner" "github.com/spf13/cobra" "github.com/zeabur/cli/internal/cmdutil" - "github.com/zeabur/cli/internal/util" "github.com/zeabur/cli/pkg/constant" ) @@ -25,20 +24,15 @@ type Options struct { func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "deploy", Short: "Deploy a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetProject(), &opts.projectID, new(string)), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDeploy(f, opts) }, } + cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID") cmd.Flags().StringVar(&opts.name, "name", "", "Service Name") cmd.Flags().StringVar(&opts.template, "template", "", "Service template") cmd.Flags().StringVar(&opts.marketplaceCode, "marketplace-code", "", "Marketplace item code") @@ -58,6 +52,10 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { } func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.projectID == "" { + return fmt.Errorf("--project-id is required") + } + err := paramCheck(opts) if err != nil { return err @@ -77,7 +75,7 @@ func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { f.Log.Infof("Service %s created", service.Name) return nil case "GIT": - _, err = f.ApiClient.CreateService(context.Background(), f.Config.GetContext().GetProject().GetID(), opts.name, opts.repoID, opts.branchName) + _, err = f.ApiClient.CreateService(context.Background(), opts.projectID, opts.name, opts.repoID, opts.branchName) if err != nil { return fmt.Errorf("create service failed: %w", err) } @@ -91,8 +89,10 @@ func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { func runDeployInteractive(f *cmdutil.Factory, opts *Options) error { // fill project id if not set by asking user - if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { - return err + if opts.projectID == "" { + if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { + return err + } } if opts.template == "" { @@ -210,7 +210,7 @@ func runDeployInteractive(f *cmdutil.Factory, opts *Options) error { ) s.Start() - _, err = f.ApiClient.CreateService(context.Background(), f.Config.GetContext().GetProject().GetID(), opts.name, opts.repoID, opts.branchName) + _, err = f.ApiClient.CreateService(context.Background(), opts.projectID, opts.name, opts.repoID, opts.branchName) if err != nil { return err } diff --git a/internal/cmd/service/exec/exec.go b/internal/cmd/service/exec/exec.go new file mode 100644 index 0000000..9bd8a62 --- /dev/null +++ b/internal/cmd/service/exec/exec.go @@ -0,0 +1,110 @@ +package exec + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/zeabur/cli/internal/cmdutil" + "github.com/zeabur/cli/internal/util" + "github.com/zeabur/cli/pkg/fill" +) + +type Options struct { + id string + name string + + environmentID string + + command []string +} + +func NewCmdExec(f *cmdutil.Factory) *cobra.Command { + opts := &Options{} + + cmd := &cobra.Command{ + Use: "exec -- [args...]", + Short: "Execute a command in a service container", + Long: "Execute a command in a running service's container. The command and its arguments should be specified after \"--\".", + Example: ` # List files in the service container + zeabur service exec -- ls -la + + # Run a shell command + zeabur service exec -- sh -c "echo hello" + + # Specify service by name + zeabur service exec --name my-svc --env-id xxx -- cat /etc/hostname`, + RunE: func(cmd *cobra.Command, args []string) error { + opts.command = cmd.Flags().Args() + if len(opts.command) == 0 { + return fmt.Errorf("command is required, use -- to separate it from flags") + } + return runExec(f, opts) + }, + } + + util.AddServiceParam(cmd, &opts.id, &opts.name) + util.AddEnvOfServiceParam(cmd, &opts.environmentID) + + return cmd +} + +func runExec(f *cmdutil.Factory, opts *Options) error { + if f.Interactive { + return runExecInteractive(f, opts) + } + return runExecNonInteractive(f, opts) +} + +func runExecInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + }); err != nil { + return err + } + } + + return runExecNonInteractive(f, opts) +} + +func runExecNonInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID + } + + result, err := f.ApiClient.ExecuteCommand(context.Background(), opts.id, opts.environmentID, opts.command) + if err != nil { + return fmt.Errorf("execute command failed: %w", err) + } + + fmt.Print(result.Output) + + if result.ExitCode != 0 { + os.Exit(result.ExitCode) + } + + return nil +} diff --git a/internal/cmd/service/expose/expose.go b/internal/cmd/service/expose/expose.go index 6582355..ac361e5 100644 --- a/internal/cmd/service/expose/expose.go +++ b/internal/cmd/service/expose/expose.go @@ -20,22 +20,16 @@ type Options struct { func NewCmdExpose(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "expose", Short: "Expose a service temporarily", Long: `Expose a service temporarily, default 3600 seconds. example: - zeabur service expose # cli will try to get service from context or prompt to select one + zeabur service expose # cli will prompt to select a service zeabur service expose --id xxxxx --env-id xxxx # use id and env-id to expose service - zeabur service expose --name xxxxx --env-id xxxx # if project context is set, use name, env-id to expose service + zeabur service expose --name xxxxx --env-id xxxx # use name, env-id to expose service `, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runExpose(f, opts) }, @@ -56,23 +50,41 @@ func runExpose(f *cmdutil.Factory, opts *Options) error { } func runExposeNonInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("please specify --id or --name") + } + + // Resolve environment and project from the service + service, err := f.ApiClient.GetService(context.Background(), opts.id, "", "", "") + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if service.Project == nil || service.Project.ID == "" { + return fmt.Errorf("service has no associated project") + } + envID, err := util.ResolveEnvironmentID(f.ApiClient, service.Project.ID) if err != nil { return err } opts.environmentID = envID } - err := paramCheck(opts) - if err != nil { - return err + projectID := "" + if service.Project != nil { + projectID = service.Project.ID } ctx := context.Background() - projectID := f.Config.GetContext().GetProject().GetID() - tempTCPPort, err := f.ApiClient.ExposeService(ctx, opts.id, opts.environmentID, projectID, opts.name) if err != nil { return fmt.Errorf("failed to expose service: %w", err) @@ -97,15 +109,3 @@ func runExposeInteractive(f *cmdutil.Factory, opts *Options) error { return runExposeNonInteractive(f, opts) } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("please specify --id or --name") - } - - if opts.environmentID == "" { - return fmt.Errorf("please specify --env-id") - } - - return nil -} diff --git a/internal/cmd/service/get/get.go b/internal/cmd/service/get/get.go index 87978f0..0b70576 100644 --- a/internal/cmd/service/get/get.go +++ b/internal/cmd/service/get/get.go @@ -22,16 +22,10 @@ type Options struct { func NewCmdGet(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "get", Short: "Get a service, if environment is specified, get the service details in the environment", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runGet(f, opts) }, @@ -64,8 +58,17 @@ func runGetInteractive(f *cmdutil.Factory, opts *Options) error { } func runGetNonInteractive(f *cmdutil.Factory, opts *Options) error { - projectName := f.Config.GetContext().GetProject().GetName() - username := f.Config.GetUsername() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } var ( t model.Tabler @@ -73,9 +76,9 @@ func runGetNonInteractive(f *cmdutil.Factory, opts *Options) error { ) if opts.environmentID == "" { - t, err = getServiceBrief(f.ApiClient, opts.id, username, projectName, opts.name) + t, err = getServiceBrief(f.ApiClient, opts.id) } else { - t, err = getServiceDetails(f.ApiClient, opts.id, username, projectName, opts.name, opts.environmentID) + t, err = getServiceDetails(f.ApiClient, opts.id, opts.environmentID) } if err != nil { @@ -87,9 +90,9 @@ func runGetNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } -func getServiceBrief(client api.ServiceAPI, id, username, projectName, name string) (t model.Tabler, err error) { +func getServiceBrief(client api.ServiceAPI, id string) (t model.Tabler, err error) { ctx := context.Background() - service, err := client.GetService(ctx, id, username, projectName, name) + service, err := client.GetService(ctx, id, "", "", "") if err != nil { return nil, fmt.Errorf("get service failed: %w", err) } @@ -97,9 +100,9 @@ func getServiceBrief(client api.ServiceAPI, id, username, projectName, name stri return service, nil } -func getServiceDetails(client api.ServiceAPI, id, username, projectID, name, environmentID string) (t model.Tabler, err error) { +func getServiceDetails(client api.ServiceAPI, id, environmentID string) (t model.Tabler, err error) { ctx := context.Background() - serviceDetail, err := client.GetServiceDetailByEnvironment(ctx, id, username, projectID, name, environmentID) + serviceDetail, err := client.GetServiceDetailByEnvironment(ctx, id, "", "", "", environmentID) if err != nil { return nil, fmt.Errorf("get service failed: %w", err) } diff --git a/internal/cmd/service/instruction/instruction.go b/internal/cmd/service/instruction/instruction.go index ce75525..f49a151 100644 --- a/internal/cmd/service/instruction/instruction.go +++ b/internal/cmd/service/instruction/instruction.go @@ -20,17 +20,10 @@ type Options struct { func NewCmdInstruction(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "instruction", Short: "Instruction for prebuiltservice", Long: `Instruction for prebuilt service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runInstruction(f, opts) }, @@ -43,22 +36,20 @@ func NewCmdInstruction(f *cmdutil.Factory) *cobra.Command { } func runInstruction(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - FilterFunc: func(service *model.Service) bool { - return service.Template == "PREBUILT" - }, - }); err != nil { - return err - } - - if err := paramCheck(opts); err != nil { - return err + if f.Interactive && opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + FilterFunc: func(service *model.Service) bool { + return service.Template == "PREBUILT" + }, + }); err != nil { + return err + } } if opts.id == "" && opts.name != "" { @@ -69,6 +60,18 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { opts.id = service.ID } + if opts.id == "" { + return fmt.Errorf("service id or name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID + } + instructions, err := f.ApiClient.ServiceInstructions(context.Background(), opts.id, opts.environmentID) if err != nil { return err @@ -80,11 +83,3 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("service id or name is required") - } - - return nil -} diff --git a/internal/cmd/service/list/list.go b/internal/cmd/service/list/list.go index 38f52de..78cbe9d 100644 --- a/internal/cmd/service/list/list.go +++ b/internal/cmd/service/list/list.go @@ -15,10 +15,7 @@ type Options struct { } func NewCmdList(f *cmdutil.Factory) *cobra.Command { - opts := &Options{ - projectID: f.Config.GetContext().GetProject().GetID(), - } - ctx := f.Config.GetContext() + opts := &Options{} cmd := &cobra.Command{ Use: "list", @@ -26,16 +23,12 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { Long: `List services, if env-id is provided, list services in the environment in detail`, Args: cobra.NoArgs, Aliases: []string{"ls"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f, &opts.projectID), - util.DefaultIDByContext(ctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runList(f, opts) }, } - cmd.Flags().StringVar(&opts.projectID, "project-id", opts.projectID, "Project ID") + cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID") util.AddEnvOfServiceParam(cmd, &opts.environmentID) return cmd @@ -50,30 +43,24 @@ func runList(f *cmdutil.Factory, opts *Options) error { } func runListInteractive(f *cmdutil.Factory, opts *Options) error { - // if project id is not set by flag, fetch from context if opts.projectID == "" { - opts.projectID = f.Config.GetContext().GetProject().GetID() - } - - // if project id is still not set, prompt to select one - if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { - return err + if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { + return err + } } return runListNonInteractive(f, opts) } func runListNonInteractive(f *cmdutil.Factory, opts *Options) error { - err := paramCheck(opts) - if err != nil { - return err + if opts.projectID == "" { + return fmt.Errorf("--project-id is required") } if opts.environmentID == "" { return listServicesBrief(f, opts.projectID) - } else { - return listServicesDetailByEnvironment(f, opts.projectID, opts.environmentID) } + return listServicesDetailByEnvironment(f, opts.projectID, opts.environmentID) } func listServicesBrief(f *cmdutil.Factory, projectID string) error { @@ -107,11 +94,3 @@ func listServicesDetailByEnvironment(f *cmdutil.Factory, projectID, environmentI return nil } - -func paramCheck(opts *Options) error { - if opts.projectID == "" { - return fmt.Errorf("project-id is required") - } - - return nil -} diff --git a/internal/cmd/service/metric/metric.go b/internal/cmd/service/metric/metric.go index e1fd2d6..bc2e659 100644 --- a/internal/cmd/service/metric/metric.go +++ b/internal/cmd/service/metric/metric.go @@ -24,8 +24,6 @@ type Options struct { func NewCmdMetric(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "metric [CPU|MEMORY|NETWORK]", Short: "Show metric of a service", @@ -36,11 +34,6 @@ func NewCmdMetric(f *cmdutil.Factory) *cobra.Command { string(model.MetricTypeMemory), string(model.MetricTypeNetwork), }, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { opts.metricType = args[0] return runMetric(f, opts) @@ -79,36 +72,48 @@ func runMetricInteractive(f *cmdutil.Factory, opts *Options) error { } func runMetricNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := paramCheck(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + // Resolve environment and project from the service + service, err := f.ApiClient.GetService(context.Background(), opts.id, "", "", "") + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + + projectID := "" + if service.Project != nil { + projectID = service.Project.ID + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } - upperCaseMetricType := strings.ToUpper(opts.metricType) + if opts.metricType == "" { + return fmt.Errorf("metric type is required") + } + upperCaseMetricType := strings.ToUpper(opts.metricType) mt := model.MetricType(opts.metricType) startTime := time.Now().Add(-time.Duration(opts.hour) * time.Hour) endTime := time.Now() - metrics, err := f.ApiClient.ServiceMetric(context.Background(), opts.id, f.Config.GetContext().GetProject().GetID(), opts.environmentID, upperCaseMetricType, startTime, endTime) + metrics, err := f.ApiClient.ServiceMetric(context.Background(), opts.id, projectID, opts.environmentID, upperCaseMetricType, startTime, endTime) if err != nil { return fmt.Errorf("get service metric failed: %w", err) } @@ -147,19 +152,3 @@ func runMetricNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - if opts.metricType == "" { - return fmt.Errorf("metric type is required") - } - - return nil -} diff --git a/internal/cmd/service/network/network.go b/internal/cmd/service/network/network.go index bbda132..adf0b37 100644 --- a/internal/cmd/service/network/network.go +++ b/internal/cmd/service/network/network.go @@ -20,20 +20,13 @@ type Options struct { func NewCmdPrivateNetwork(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "network", Short: "Network information for service", Long: `Network information for service`, Aliases: []string{"net"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { - return runInstruction(f, opts) + return runNetwork(f, opts) }, } @@ -43,20 +36,18 @@ func NewCmdPrivateNetwork(f *cmdutil.Factory) *cobra.Command { return cmd } -func runInstruction(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err - } - - if err := paramCheck(opts); err != nil { - return err +func runNetwork(f *cmdutil.Factory, opts *Options) error { + if f.Interactive && opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + }); err != nil { + return err + } } if opts.id == "" && opts.name != "" { @@ -67,6 +58,10 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { opts.id = service.ID } + if opts.id == "" { + return fmt.Errorf("service id or name is required") + } + s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, spinner.WithColor(cmdutil.SpinnerColor), spinner.WithSuffix(fmt.Sprintf(" Fetching network information of service %s ...", opts.name)), @@ -85,11 +80,3 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("service id or name is required") - } - - return nil -} diff --git a/internal/cmd/service/redeploy/redeploy.go b/internal/cmd/service/redeploy/redeploy.go index 7471af1..f39ca8d 100644 --- a/internal/cmd/service/redeploy/redeploy.go +++ b/internal/cmd/service/redeploy/redeploy.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdRedeploy(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "redeploy", Short: "redeploy a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runRedeploy(f, opts) }, @@ -68,26 +62,24 @@ func runRedeployInteractive(f *cmdutil.Factory, opts *Options) error { } func runRedeployNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -116,15 +108,3 @@ func runRedeployNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/restart/restart.go b/internal/cmd/service/restart/restart.go index 318d404..d407ec1 100644 --- a/internal/cmd/service/restart/restart.go +++ b/internal/cmd/service/restart/restart.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdRestart(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "restart", Short: "restart a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runRestart(f, opts) }, @@ -68,26 +62,24 @@ func runRestartInteractive(f *cmdutil.Factory, opts *Options) error { } func runRestartNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -116,15 +108,3 @@ func runRestartNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/service.go b/internal/cmd/service/service.go index 026f5ed..ae22b01 100644 --- a/internal/cmd/service/service.go +++ b/internal/cmd/service/service.go @@ -6,6 +6,7 @@ import ( serviceDeleteCmd "github.com/zeabur/cli/internal/cmd/service/delete" serviceDeployCmd "github.com/zeabur/cli/internal/cmd/service/deploy" + serviceExecCmd "github.com/zeabur/cli/internal/cmd/service/exec" serviceExposeCmd "github.com/zeabur/cli/internal/cmd/service/expose" serviceGetCmd "github.com/zeabur/cli/internal/cmd/service/get" serviceInstructionCmd "github.com/zeabur/cli/internal/cmd/service/instruction" @@ -36,6 +37,7 @@ func NewCmdService(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(serviceSuspendCmd.NewCmdSuspend(f)) cmd.AddCommand(serviceDeleteCmd.NewCmdDelete(f)) cmd.AddCommand(serviceDeployCmd.NewCmdDeploy(f)) + cmd.AddCommand(serviceExecCmd.NewCmdExec(f)) cmd.AddCommand(serviceInstructionCmd.NewCmdInstruction(f)) cmd.AddCommand(serviceNetworkCmd.NewCmdPrivateNetwork(f)) cmd.AddCommand(serviceUpdateCmd.NewCmdUpdate(f)) diff --git a/internal/cmd/service/suspend/suspend.go b/internal/cmd/service/suspend/suspend.go index c64e3de..a96a729 100644 --- a/internal/cmd/service/suspend/suspend.go +++ b/internal/cmd/service/suspend/suspend.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdSuspend(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "suspend", Short: "suspend a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runSuspend(f, opts) }, @@ -68,26 +62,24 @@ func runSuspendInteractive(f *cmdutil.Factory, opts *Options) error { } func runSuspendNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -116,15 +108,3 @@ func runSuspendNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/update/tag/tag.go b/internal/cmd/service/update/tag/tag.go index 97f3727..043f201 100644 --- a/internal/cmd/service/update/tag/tag.go +++ b/internal/cmd/service/update/tag/tag.go @@ -24,16 +24,10 @@ type Options struct { func NewCmdTag(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "tag", Short: "Update image tag of a prebuilt service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runUpdate(f, opts) }, @@ -83,26 +77,28 @@ func runInteractive(f *cmdutil.Factory, opts *Options) error { } func runNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID + } + + if opts.tag == "" { + return fmt.Errorf("--tag is required") } idOrName := opts.name @@ -128,19 +124,3 @@ func runNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - if opts.tag == "" { - return fmt.Errorf("--tag is required") - } - - return nil -} diff --git a/internal/cmd/template/deploy/deploy.go b/internal/cmd/template/deploy/deploy.go index f9b8720..d928be9 100644 --- a/internal/cmd/template/deploy/deploy.go +++ b/internal/cmd/template/deploy/deploy.go @@ -23,6 +23,7 @@ import ( type Options struct { file string projectID string + region string skipValidation bool vars map[string]string } @@ -40,6 +41,7 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { cmd.Flags().StringVarP(&opts.file, "file", "f", "", "Template file") cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID to deploy on") + cmd.Flags().StringVarP(&opts.region, "region", "r", "", "Region to create a new project in (e.g. tpe0, sfo0)") cmd.Flags().BoolVar(&opts.skipValidation, "skip-validation", false, "Skip template validation") cmd.Flags().StringToStringVar(&opts.vars, "var", nil, "Template variables (e.g. --var KEY=value)") @@ -88,7 +90,14 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { } } - if _, err := f.ParamFiller.ProjectCreatePreferred(&opts.projectID); err != nil { + if opts.region != "" && opts.projectID == "" { + project, err := f.ApiClient.CreateProject(context.Background(), opts.region, nil) + if err != nil { + return fmt.Errorf("create project in region %s: %w", opts.region, err) + } + opts.projectID = project.ID + f.Log.Infof("Created project %q in region %s.", project.ID, opts.region) + } else if _, err := f.ParamFiller.ProjectCreatePreferred(&opts.projectID); err != nil { return err } diff --git a/internal/cmd/upload/upload.go b/internal/cmd/upload/upload.go index 2378d75..4614ebe 100644 --- a/internal/cmd/upload/upload.go +++ b/internal/cmd/upload/upload.go @@ -27,7 +27,6 @@ func NewCmdUpload(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "upload", Short: "Upload local project to Zeabur", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runUpload(f, opts) }, diff --git a/internal/cmd/variable/create/create.go b/internal/cmd/variable/create/create.go index 3956d9c..a72439c 100644 --- a/internal/cmd/variable/create/create.go +++ b/internal/cmd/variable/create/create.go @@ -23,17 +23,11 @@ type Options struct { func NewCmdCreateVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "create", Short: "create variable(s)", Long: `Create variable(s) for a service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runCreateVariable(f, opts) }, @@ -92,16 +86,24 @@ func runCreateVariableInteractive(f *cmdutil.Factory, opts *Options) error { } func runCreateVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/variable/delete/delete.go b/internal/cmd/variable/delete/delete.go index 7cc04a1..708c2af 100644 --- a/internal/cmd/variable/delete/delete.go +++ b/internal/cmd/variable/delete/delete.go @@ -23,18 +23,12 @@ type Options struct { func NewCmdDeleteVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "delete", Short: "delete variable(s)", Long: `delete variable(s) for a service`, Aliases: []string{"del"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDeleteVariable(f, opts) }, @@ -111,16 +105,24 @@ func runDeleteVariableInteractive(f *cmdutil.Factory, opts *Options) error { } func runDeleteVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } for _, v := range opts.deleteKeys { diff --git a/internal/cmd/variable/env/env.go b/internal/cmd/variable/env/env.go index 97b0480..5084d67 100644 --- a/internal/cmd/variable/env/env.go +++ b/internal/cmd/variable/env/env.go @@ -22,17 +22,11 @@ type Options struct { func NewCmdEnvVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "env", Short: "update variables from .env", Long: "overwrite variables from a .env file", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runUpdateVariableByEnv(f, opts) }, @@ -54,20 +48,41 @@ func runUpdateVariableByEnv(f *cmdutil.Factory, opts *Options) error { return fmt.Errorf("file cannot open: %s (%w)", opts.envFilename, err) } + if f.Interactive && opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + }); err != nil { + return err + } + } + return runUpdateVariableNonInteractive(f, opts) } func runUpdateVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/variable/list/list.go b/internal/cmd/variable/list/list.go index d5e4944..6f90c83 100644 --- a/internal/cmd/variable/list/list.go +++ b/internal/cmd/variable/list/list.go @@ -19,7 +19,6 @@ type Options struct { func NewCmdListVariables(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "list", @@ -27,11 +26,6 @@ func NewCmdListVariables(f *cmdutil.Factory) *cobra.Command { Long: `List environment variables of a service`, Args: cobra.NoArgs, Aliases: []string{"ls"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runListVariables(f, opts) }, @@ -68,21 +62,29 @@ func runListVariablesInteractive(f *cmdutil.Factory, opts *Options) error { } func runListVariablesNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, spinner.WithColor(cmdutil.SpinnerColor), - spinner.WithSuffix(fmt.Sprintf(" Fetching environment variablesof service %s ...", opts.name)), + spinner.WithSuffix(fmt.Sprintf(" Fetching environment variables of service %s ...", opts.name)), ) s.Start() variableList, readonlyVariableList, err := f.ApiClient.ListVariables(context.Background(), opts.id, opts.environmentID) diff --git a/internal/cmd/variable/update/update.go b/internal/cmd/variable/update/update.go index 94c6589..c5fe442 100644 --- a/internal/cmd/variable/update/update.go +++ b/internal/cmd/variable/update/update.go @@ -22,17 +22,11 @@ type Options struct { func NewCmdUpdateVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "update", Short: "update variable(s)", Long: `update variable(s) for a service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runUpdateVariable(f, opts) }, @@ -113,16 +107,24 @@ func runUpdateVariableInteractive(f *cmdutil.Factory, opts *Options) error { } func runUpdateVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/util/env.go b/internal/util/env.go index 6c4e52b..964c02e 100644 --- a/internal/util/env.go +++ b/internal/util/env.go @@ -22,7 +22,7 @@ func AddEnvOfServiceParam(cmd *cobra.Command, id *string) { // Every project has exactly one environment since environments are deprecated. func ResolveEnvironmentID(client api.Client, projectID string) (string, error) { if projectID == "" { - return "", fmt.Errorf("project ID is required to resolve environment ID; please set project context with `zeabur context set project`") + return "", fmt.Errorf("project ID is required to resolve environment ID") } environments, err := client.ListEnvironments(context.Background(), projectID) @@ -36,3 +36,16 @@ func ResolveEnvironmentID(client api.Client, projectID string) (string, error) { return environments[0].ID, nil } + +// ResolveEnvironmentIDByServiceID fetches the service, finds its project, +// and resolves the environment ID from that project. +func ResolveEnvironmentIDByServiceID(client api.Client, serviceID string) (string, error) { + service, err := client.GetService(context.Background(), serviceID, "", "", "") + if err != nil { + return "", fmt.Errorf("get service failed: %w", err) + } + if service.Project == nil || service.Project.ID == "" { + return "", fmt.Errorf("service has no associated project") + } + return ResolveEnvironmentID(client, service.Project.ID) +} diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 7529a2c..eacff24 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -69,6 +69,7 @@ type ( GetDNSName(ctx context.Context, serviceID string) (string, error) UpdateImageTag(ctx context.Context, serviceID string, environmentID string, tag string) error DeleteService(ctx context.Context, id string, environmentID string) error + ExecuteCommand(ctx context.Context, serviceID string, environmentID string, command []string) (*model.CommandResult, error) } VariableAPI interface { diff --git a/pkg/api/service.go b/pkg/api/service.go index 8e23dc8..244538b 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -530,3 +530,20 @@ func (c *client) DeleteService(ctx context.Context, id string, environmentID str return err } + +func (c *client) ExecuteCommand(ctx context.Context, serviceID string, environmentID string, command []string) (*model.CommandResult, error) { + var mutation struct { + ExecuteCommand model.CommandResult `graphql:"executeCommand(serviceID: $serviceID, environmentID: $environmentID, command: $command)"` + } + + err := c.Mutate(ctx, &mutation, V{ + "serviceID": ObjectID(serviceID), + "environmentID": ObjectID(environmentID), + "command": command, + }) + if err != nil { + return nil, err + } + + return &mutation.ExecuteCommand, nil +} diff --git a/pkg/model/command.go b/pkg/model/command.go new file mode 100644 index 0000000..5f55f58 --- /dev/null +++ b/pkg/model/command.go @@ -0,0 +1,7 @@ +package model + +// CommandResult represents the result of a command execution. +type CommandResult struct { + ExitCode int `graphql:"exitCode"` + Output string `graphql:"output"` +}