From 1380f9e55f71489bb5a712c871d94aa0dc1a87d2 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Thu, 9 Nov 2023 16:07:15 +0800 Subject: [PATCH 01/13] feat: use deploy as a default command if no command is specified --- cmd/main.go | 8 +++- internal/cmd/deploy/deploy.go | 79 ++--------------------------------- internal/cmd/root/root.go | 21 ++++++++-- pkg/api/deploy.go | 1 + 4 files changed, 29 insertions(+), 80 deletions(-) create mode 100644 pkg/api/deploy.go diff --git a/cmd/main.go b/cmd/main.go index fa55b70..0ebb37e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,9 @@ package main import ( + "os" + "time" + "github.com/zeabur/cli/internal/cmd/root" "github.com/zeabur/cli/internal/cmdutil" "github.com/zeabur/cli/pkg/auth" @@ -9,7 +12,6 @@ import ( "github.com/zeabur/cli/pkg/log" "github.com/zeabur/cli/pkg/printer" "github.com/zeabur/cli/pkg/prompt" - "time" ) var ( @@ -26,6 +28,10 @@ func main() { panic(err) } + if len(os.Args) <= 1 { + os.Args = append([]string{os.Args[0], "deploy"}, os.Args[1:]...) + } + // log errors if err := rootCmd.Execute(); err != nil { // when some errors occur(such as args dis-match), the log may not be initialized diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go index fc8cb12..1d64c2b 100644 --- a/internal/cmd/deploy/deploy.go +++ b/internal/cmd/deploy/deploy.go @@ -1,13 +1,10 @@ package deploy import ( - "context" - "github.com/briandowns/spinner" "github.com/spf13/cobra" "github.com/zeabur/cli/internal/cmdutil" "github.com/zeabur/cli/internal/util" - "golang.org/x/sync/errgroup" ) type Options struct { @@ -19,7 +16,7 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "deploy", - Short: "Deploy a local Git Service", + Short: "Deploy local codes to Zeabur with one command", PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runDeploy(f, opts) @@ -40,20 +37,7 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { } func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { - repoOwner, repoName, err := f.ApiClient.GetRepoInfo() - if err != nil { - return err - } - - repoID, err := f.ApiClient.GetRepoID(repoOwner, repoName) - if err != nil { - return err - } - - f.Log.Debugf("repoID: %d", repoID) - - //TODO: Deploy Local Git Service NonInteractive - + f.Log.Info("Deploying...") return nil } @@ -64,65 +48,8 @@ func runDeployInteractive(f *cmdutil.Factory, opts *Options) error { ) s.Start() - var repoOwner string - var repoName string - var err error - - repoOwner, repoName, err = f.ApiClient.GetRepoInfo() - if err != nil { - return err - } - - // Use repo name as default service name - if opts.name == "" { - opts.name = repoName - } - - var eg errgroup.Group - var repoID int - var branches []string - - eg.Go(func() error { - repoID, err = f.ApiClient.GetRepoID(repoOwner, repoName) - return err - }) - - eg.Go(func() error { - branches, err = f.ApiClient.GetRepoBranches(context.Background(), repoOwner, repoName) - return err - }) + f.Log.Info("Deploying interactive...") - if err = eg.Wait(); err != nil { - return err - } - - s.Stop() - - // If repo has only one branch, use it as default branch - // Otherwise, ask user to select a branch - var branch string - - if len(branches) == 1 { - branch = branches[0] - } else { - _, err = f.Prompter.Select("Select branch", branch, branches) - if err != nil { - return err - } - } - - s = spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, - spinner.WithColor(cmdutil.SpinnerColor), - spinner.WithSuffix(" Creating service..."), - spinner.WithFinalMSG(cmdutil.SuccessIcon+" Service created 🥂\n"), - ) - s.Start() - - _, err = f.ApiClient.CreateService(context.Background(), f.Config.GetContext().GetProject().GetID(), opts.name, repoID, branch) - if err != nil { - return err - } s.Stop() - return nil } diff --git a/internal/cmd/root/root.go b/internal/cmd/root/root.go index af5528b..1c3f658 100644 --- a/internal/cmd/root/root.go +++ b/internal/cmd/root/root.go @@ -2,12 +2,12 @@ package root import ( - "errors" "fmt" "time" "github.com/zeabur/cli/pkg/fill" "github.com/zeabur/cli/pkg/selector" + "golang.org/x/oauth2" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -31,7 +31,7 @@ import ( // NewCmdRoot creates the root command func NewCmdRoot(f *cmdutil.Factory, version, commit, date string) (*cobra.Command, error) { cmd := &cobra.Command{ - Use: "zeabur [flags]", + Use: "zeabur", Short: "Zeabur CLI", Long: `Zeabur CLI is the official command line tool for Zeabur.`, Example: heredoc.Doc(` @@ -50,8 +50,23 @@ func NewCmdRoot(f *cmdutil.Factory, version, commit, date string) (*cobra.Comman // require that the user is authenticated before running most commands if cmdutil.IsAuthCheckEnabled(cmd) { f.Log.Debug("Checking authentication") + // do not return error, guide user to login instead if !f.LoggedIn() { - return errors.New("not authenticated") + f.Log.Info("A browser window will be opened for you to login, please confirm") + + var ( + tokenString string + token *oauth2.Token + err error + ) + + token, err = f.AuthClient.Login() + if err != nil { + return fmt.Errorf("failed to login: %w", err) + } + tokenString = token.AccessToken + f.Config.SetToken(token) + f.Config.SetTokenString(tokenString) } // set up the client f.ApiClient = api.New(f.Config.GetTokenString()) diff --git a/pkg/api/deploy.go b/pkg/api/deploy.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/pkg/api/deploy.go @@ -0,0 +1 @@ +package api From 9fe3e197046cabcbed977986da700ae89bd3f7b9 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Thu, 9 Nov 2023 17:34:35 +0800 Subject: [PATCH 02/13] feat: select or create project before deploy --- internal/cmd/deploy/deploy.go | 49 +++++++++++++++++++++++------------ internal/cmd/root/root.go | 1 - 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go index 1d64c2b..16c2a29 100644 --- a/internal/cmd/deploy/deploy.go +++ b/internal/cmd/deploy/deploy.go @@ -1,6 +1,8 @@ package deploy import ( + "context" + "github.com/briandowns/spinner" "github.com/spf13/cobra" "github.com/zeabur/cli/internal/cmdutil" @@ -29,27 +31,42 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { } func runDeploy(f *cmdutil.Factory, opts *Options) error { - if f.Interactive { - return runDeployInteractive(f, opts) - } else { - return runDeployNonInteractive(f, opts) - } -} - -func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { - f.Log.Info("Deploying...") - return nil -} - -func runDeployInteractive(f *cmdutil.Factory, opts *Options) error { s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, spinner.WithColor(cmdutil.SpinnerColor), - spinner.WithSuffix(" Fetching repository information..."), + spinner.WithSuffix(" Fetching projects ..."), ) s.Start() + projects, err := f.ApiClient.ListAllProjects(context.Background()) + if err != nil { + return err + } + s.Stop() + + if len(projects) == 0 { + confirm, err := f.Prompter.Confirm("No projects found, would you like to create one now?", true) + if err != nil { + return err + } + if confirm { + project, err := f.ApiClient.CreateProject(context.Background(), "default", nil) + if err != nil { + f.Log.Error("Failed to create project: ", err) + return err + } + f.Log.Infof("Project %s created", project.Name) + //TODO: Deploy codes as a service in the project + return nil + } + } - f.Log.Info("Deploying interactive...") + f.Log.Info("Select one project to deploy your service.") + + _, project, err := f.Selector.SelectProject() + if err != nil { + return err + } + + f.Log.Info("You have selected project %s", project.Name) - s.Stop() return nil } diff --git a/internal/cmd/root/root.go b/internal/cmd/root/root.go index 1c3f658..1447285 100644 --- a/internal/cmd/root/root.go +++ b/internal/cmd/root/root.go @@ -49,7 +49,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, commit, date string) (*cobra.Comman // require that the user is authenticated before running most commands if cmdutil.IsAuthCheckEnabled(cmd) { - f.Log.Debug("Checking authentication") // do not return error, guide user to login instead if !f.LoggedIn() { f.Log.Info("A browser window will be opened for you to login, please confirm") From a3a8b4fb986150ba914b4c68f6559b0eceacea36 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Mon, 13 Nov 2023 11:24:24 +0800 Subject: [PATCH 03/13] feat: set project context after create or select --- internal/cmd/deploy/deploy.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go index 16c2a29..c11b651 100644 --- a/internal/cmd/deploy/deploy.go +++ b/internal/cmd/deploy/deploy.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/zeabur/cli/internal/cmdutil" "github.com/zeabur/cli/internal/util" + "github.com/zeabur/cli/pkg/zcontext" ) type Options struct { @@ -18,7 +19,7 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "deploy", - Short: "Deploy local codes to Zeabur with one command", + 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) @@ -54,7 +55,8 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { return err } f.Log.Infof("Project %s created", project.Name) - //TODO: Deploy codes as a service in the project + f.Config.GetContext().SetProject(zcontext.NewBasicInfo(project.ID, project.Name)) + return nil } } @@ -68,5 +70,7 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { f.Log.Info("You have selected project %s", project.Name) + f.Config.GetContext().SetProject(zcontext.NewBasicInfo(project.ID, project.Name)) + return nil } From 9edff842638861e527c6350e5c41b2ac27af65eb Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Fri, 1 Dec 2023 17:51:03 +0800 Subject: [PATCH 04/13] fix: query user without lastCheckIn --- .gitignore | 3 ++- pkg/auth/const.go | 4 ++-- pkg/model/user.go | 6 ++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 92cc726..30d3ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .idea/ .vscode/ +.env # test && mocks mocks/ @@ -9,4 +10,4 @@ mocks/ zeabur # dist -dist/ +dist/ \ No newline at end of file diff --git a/pkg/auth/const.go b/pkg/auth/const.go index 0a860b3..bcaad89 100644 --- a/pkg/auth/const.go +++ b/pkg/auth/const.go @@ -8,8 +8,8 @@ const ( ZeaburOAuthAuthorizeURL = ZeaburOAuthServerURL + "/authorize" ZeaburOAuthTokenURL = ZeaburOAuthServerURL + "/token" - ZeaburOAuthCLIClientID = "64c7559d0e6da9ed35dee1ff" - ZeaburOAuthCLIClientSecret = "630641" + ZeaburOAuthCLIClientID = "c92dcff3-92a1-4d39-827e-a3eb952d5e0b" + ZeaburOAuthCLIClientSecret = "tI2Q32jf" OAuthLocalServerCallbackURL = "http://localhost/callback" ) diff --git a/pkg/model/user.go b/pkg/model/user.go index 78b87e5..cad9b80 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -23,8 +23,7 @@ type User struct { BannedAt *time.Time `json:"bannedAt" graphql:"bannedAt"` //BannedReason *BannedReasonType `json:"bannedReason" graphql:"bannedReason"` //EmailPreference map[string]bool `json:"emailPreference" graphql:"emailPreference"` - AgreedAt *time.Time `json:"agreedAt" graphql:"agreedAt"` - LastCheckedInAt *time.Time `json:"lastCheckedInAt" graphql:"lastCheckedInAt"` + AgreedAt *time.Time `json:"agreedAt" graphql:"agreedAt"` // DiscordID is the user's Discord ID. DiscordID *string `json:"discordID" graphql:"discordID"` ID string `json:"_id" graphql:"_id"` @@ -37,7 +36,7 @@ type User struct { } func (u *User) Header() []string { - return []string{"ID", "Name", "Username", "Email", "Language", "LastCheckInAt", "RegisteredAt"} + return []string{"ID", "Name", "Username", "Email", "Language", "RegisteredAt"} } func (u *User) Rows() [][]string { @@ -47,7 +46,6 @@ func (u *User) Rows() [][]string { row = append(row, u.Username) row = append(row, u.Email) row = append(row, u.Language) - row = append(row, u.LastCheckedInAt.Format(time.RFC3339)) row = append(row, u.CreatedAt.Format(time.RFC3339)) return [][]string{row} From 079a49fff51ba97d49e4141c8c20bb95d8abae83 Mon Sep 17 00:00:00 2001 From: goreleaserbot Date: Fri, 1 Dec 2023 09:54:29 +0000 Subject: [PATCH 05/13] Scoop update for zeabur version v0.0.7 --- zeabur.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/zeabur.json b/zeabur.json index c7bcfa4..df446d0 100644 --- a/zeabur.json +++ b/zeabur.json @@ -1,26 +1,26 @@ { - "version": "0.0.6", + "version": "0.0.7", "architecture": { "32bit": { - "url": "https://github.com/zeabur/cli/releases/download/v0.0.6/zeabur_0.0.6_windows_386.zip", + "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_386.zip", "bin": [ "zeabur.exe" ], - "hash": "b163c4517d5d99f18b7778b6c82a9610bfc904e0aa512fd58ee6cf88de79ad36" + "hash": "f5d755406af5ccad0b981f4142ded15c97e47930fc52dee6c53b3a2baeb6337c" }, "64bit": { - "url": "https://github.com/zeabur/cli/releases/download/v0.0.6/zeabur_0.0.6_windows_amd64.zip", + "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_amd64.zip", "bin": [ "zeabur.exe" ], - "hash": "af00616cc04774728437c32c5a5f08a9cf7e764ed0353511d557b8a9df79ecc6" + "hash": "670e3b54934df6c3637c313c957870d8a81af9724bb80f10c0f73fb91a89d4c6" }, "arm64": { - "url": "https://github.com/zeabur/cli/releases/download/v0.0.6/zeabur_0.0.6_windows_arm64.zip", + "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_arm64.zip", "bin": [ "zeabur.exe" ], - "hash": "30ea27aa156586c33cc79f2be7b0ed8c538f8ad9d4ac769ea91d2b7fc3321af7" + "hash": "569a2e6aa9ac2a77f0524c70471434d986c30043b3fc0003b5012b342ff98f8a" } }, "homepage": "https://zeabur.com", From e4ac7b3ee8a051f6392aba5f9db1ae30a8e53b90 Mon Sep 17 00:00:00 2001 From: goreleaserbot Date: Fri, 1 Dec 2023 10:11:06 +0000 Subject: [PATCH 06/13] Scoop update for zeabur version v0.0.7 --- zeabur.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zeabur.json b/zeabur.json index df446d0..d3c8122 100644 --- a/zeabur.json +++ b/zeabur.json @@ -6,21 +6,21 @@ "bin": [ "zeabur.exe" ], - "hash": "f5d755406af5ccad0b981f4142ded15c97e47930fc52dee6c53b3a2baeb6337c" + "hash": "5cae2460a239e20fc5bfd24632bc09895053ff35b9a669f5bb35579acd160a31" }, "64bit": { "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_amd64.zip", "bin": [ "zeabur.exe" ], - "hash": "670e3b54934df6c3637c313c957870d8a81af9724bb80f10c0f73fb91a89d4c6" + "hash": "88264c612f0644186c87ef47828463323630091365090d4abcb2e7114614e0b7" }, "arm64": { "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_arm64.zip", "bin": [ "zeabur.exe" ], - "hash": "569a2e6aa9ac2a77f0524c70471434d986c30043b3fc0003b5012b342ff98f8a" + "hash": "ba7a294c6ed76fc1f6ad7ca55be60a1471e61c1177c60a3ef5132a02a3b77b6c" } }, "homepage": "https://zeabur.com", From 4042767efe5f8ef70e1c62a9351251a5daa5aa65 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Fri, 1 Dec 2023 18:11:19 +0800 Subject: [PATCH 07/13] fix release --- .goreleaser.yaml | 12 +----------- README.md | 7 ------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 80a9606..4d5fbd4 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -46,14 +46,4 @@ brews: name: homebrew-tap token: "{{ .Env.TAP_GITHUB_TOKEN }}" homepage: "https://zeabur.com" - description: "Zeabur's official command line tool" - -scoops: - - url_template: "https://github.com/zeabur/cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - bucket: - owner: zeabur - name: cli - homepage: "https://zeabur.com" - description: "Zeabur's official command line tool" - repository: - token: "{{ .Env.TAP_GITHUB_TOKEN }}" + description: "Zeabur's official command line tool" \ No newline at end of file diff --git a/README.md b/README.md index 4c206f7..3f2b295 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,6 @@ iwr -useb https://dub.sh/zb-win | iex brew install zeabur/tap/cli ``` -### Scoop (Windows) - -```powershell -scoop bucket add zeabur https://github.com/zeabur/cli -scoop install zeabur -``` - ### 2. Login If you can open the browser: From e2964bfd50197eae4cf39b25f6be0765c3cb2e52 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Tue, 19 Dec 2023 11:36:08 +0800 Subject: [PATCH 08/13] feat: create project in seletor --- internal/cmd/project/create/create.go | 3 + internal/util/pack.go | 109 ++++++++++++++++++++++++++ pkg/selector/selector.go | 30 +++++++ 3 files changed, 142 insertions(+) create mode 100644 internal/util/pack.go diff --git a/internal/cmd/project/create/create.go b/internal/cmd/project/create/create.go index 25f78ac..067b09d 100644 --- a/internal/cmd/project/create/create.go +++ b/internal/cmd/project/create/create.go @@ -3,6 +3,7 @@ package create import ( "context" "fmt" + "github.com/zeabur/cli/pkg/zcontext" "github.com/briandowns/spinner" @@ -55,6 +56,8 @@ func runCreateInteractive(f *cmdutil.Factory, opts *Options) error { if err != nil { return err } + + regions = regions[1:] s.Stop() regionIDs := make([]string, 0, len(regions)) diff --git a/internal/util/pack.go b/internal/util/pack.go new file mode 100644 index 0000000..0639b6c --- /dev/null +++ b/internal/util/pack.go @@ -0,0 +1,109 @@ +package util + +import ( + "archive/zip" + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +func PackZip() ([]byte, error) { + zipBytes, err := wrapNodeFunction(os.Getenv("PWD"), map[string]string{}) + if err != nil { + return nil, fmt.Errorf("wrap node function: %w", err) + } + + return zipBytes, nil +} + +func wrapNodeFunction(baseFolder string, envVars map[string]string) ([]byte, error) { + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + err := filepath.Walk(baseFolder, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("walking to %s: %w", path, err) + } + + if info.IsDir() { + return nil + } + + // This will ensure only the content inside baseFolder is included at the root of the ZIP. + relativePath, err := filepath.Rel(baseFolder, path) + if err != nil { + return fmt.Errorf("getting relative path: %w", err) + } + + lstat, err := os.Lstat(path) + if err != nil { + return fmt.Errorf("lstat: %w", err) + } + + if lstat.Mode()&os.ModeSymlink == os.ModeSymlink { + + zipFile, err := w.Create(relativePath + ".link") + if err != nil { + return fmt.Errorf("creating zip file: %w", err) + } + + target, err := os.Readlink(path) + if err != nil { + return fmt.Errorf("read symlink: %w", err) + } + + _, err = zipFile.Write([]byte(target)) + if err != nil { + return fmt.Errorf("writing zip file: %w", err) + } + + } else { + + zipFile, err := w.Create(relativePath) + if err != nil { + return fmt.Errorf("creating zip file: %w", err) + } + + fileContent, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("reading file: %w", err) + } + + _, err = zipFile.Write(fileContent) + if err != nil { + return fmt.Errorf("writing zip file: %w", err) + } + + } + + zipFile, err := w.Create(".zeabur-env.json") + if err != nil { + return fmt.Errorf("creating zip file: %w", err) + } + + envJsonStr, err := json.Marshal(envVars) + if err != nil { + return fmt.Errorf("marshaling env vars: %w", err) + } + + _, err = zipFile.Write(envJsonStr) + if err != nil { + return fmt.Errorf("writing zip file: %w", err) + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("walking function directory: %w", err) + } + + err = w.Close() + if err != nil { + return nil, fmt.Errorf("closing zip writer: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 7665ba9..470874d 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -64,10 +64,40 @@ func (s *selector) SelectProject() (zcontext.BasicInfo, *model.Project, error) { for i, project := range projects { projectsName[i] = project.Name } + projectsName = append(projectsName, "Create a new project") index, err := s.prompter.Select("Select project", projectsName[0], projectsName) if err != nil { return nil, nil, fmt.Errorf("select project failed: %w", err) } + + if index == len(projects) { + + regions, err := s.client.GetRegions(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("get regions failed: %w", err) + } + regions = regions[1:] + + regionIDs := make([]string, 0, len(regions)) + for _, region := range regions { + regionIDs = append(regionIDs, region.ID) + } + + projectRegionIndex, err := s.prompter.Select("Select project region", "", regionIDs) + if err != nil { + return nil, nil, fmt.Errorf("select project region failed: %w", err) + } + + projectRegion := regions[projectRegionIndex].ID + + project, err := s.client.CreateProject(context.Background(), projectRegion, nil) + if err != nil { + return nil, nil, fmt.Errorf("create project failed: %w", err) + } + + return zcontext.NewBasicInfo(project.ID, project.Name), project, nil + } + project := projects[index] return zcontext.NewBasicInfo(project.ID, project.Name), project, nil From 0fa2c12f90b7ce5f7fb815d6b0401c5467e1cc33 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Wed, 20 Dec 2023 10:28:10 +0800 Subject: [PATCH 09/13] feat: upload local codes to dpeloy --- internal/cmd/deploy/deploy.go | 40 +++++++++++++++++- internal/util/pack.go | 12 ++++-- pkg/api/interface.go | 2 + pkg/api/service.go | 78 +++++++++++++++++++++++++++++++++++ pkg/auth/client.go | 2 +- 5 files changed, 128 insertions(+), 6 deletions(-) diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go index c11b651..82eb960 100644 --- a/internal/cmd/deploy/deploy.go +++ b/internal/cmd/deploy/deploy.go @@ -2,6 +2,7 @@ package deploy import ( "context" + "fmt" "github.com/briandowns/spinner" "github.com/spf13/cobra" @@ -68,9 +69,44 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { return err } - f.Log.Info("You have selected project %s", project.Name) - f.Config.GetContext().SetProject(zcontext.NewBasicInfo(project.ID, project.Name)) + _, environment, err := f.Selector.SelectEnvironment(project.ID) + if err != nil { + return err + } + + s = spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, + spinner.WithColor(cmdutil.SpinnerColor), + spinner.WithSuffix(" Creating new service ..."), + ) + s.Start() + + bytes, fileName, err := util.PackZip() + if err != nil { + return err + } + + service, err := f.ApiClient.CreateEmptyService(context.Background(), project.ID, fileName) + if err != nil { + return err + } + + s.Stop() + + s = spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, + spinner.WithColor(cmdutil.SpinnerColor), + spinner.WithSuffix(" Uploading codes to Zeabur ..."), + ) + s.Start() + + _, err = f.ApiClient.UploadZipToService(context.Background(), project.ID, service.ID, environment.ID, bytes) + if err != nil { + return err + } + s.Stop() + + fmt.Println("Service created successfully, you can access it at: ", "https://dash.zeabur.com/projects/"+project.ID+"/services/"+service.ID+"?environmentID="+environment.ID) + return nil } diff --git a/internal/util/pack.go b/internal/util/pack.go index 0639b6c..9952276 100644 --- a/internal/util/pack.go +++ b/internal/util/pack.go @@ -9,13 +9,19 @@ import ( "path/filepath" ) -func PackZip() ([]byte, error) { +func PackZip() ([]byte, string, error) { zipBytes, err := wrapNodeFunction(os.Getenv("PWD"), map[string]string{}) + + // turn pwd to a valid file name + fileName := filepath.Base(os.Getenv("PWD")) + if err != nil { - return nil, fmt.Errorf("wrap node function: %w", err) + return nil, "", fmt.Errorf("wrap node function: %w", err) } - return zipBytes, nil + os.WriteFile("zeabur.zip", zipBytes, 0644) + + return zipBytes, fileName, nil } func wrapNodeFunction(baseFolder string, envVars map[string]string) ([]byte, error) { diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 96c6be6..a12433c 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -56,6 +56,8 @@ type ( ExposeService(ctx context.Context, id string, environmentID string, projectID string, name string) (*model.TempTCPPort, error) CreateServiceFromMarketplace(ctx context.Context, projectID string, name string, itemCode string) (*model.Service, error) CreateService(ctx context.Context, projectID string, name string, repoID int, branchName string) (*model.Service, error) + CreateEmptyService(ctx context.Context, projectID string, name string) (*model.Service, error) + UploadZipToService(ctx context.Context, projectID string, serviceID string, environmentID string, zipBytes []byte) (*model.Service, error) } DeploymentAPI interface { diff --git a/pkg/api/service.go b/pkg/api/service.go index 62d9d70..3a1ac29 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -1,10 +1,16 @@ package api import ( + "bytes" "context" "errors" + "fmt" + "io" + "mime/multipart" + "net/http" "time" + "github.com/spf13/viper" "github.com/zeabur/cli/pkg/model" ) @@ -305,3 +311,75 @@ func (c *client) CreateService(ctx context.Context, projectID string, name strin return &mutation.CreateService, nil } + +func (c *client) CreateEmptyService(ctx context.Context, projectID string, name string) (*model.Service, error) { + var mutation struct { + CreateService model.Service `graphql:"createService(projectID: $projectID, template: $template, name: $name)"` + } + + err := c.Mutate(ctx, &mutation, V{ + "projectID": ObjectID(projectID), + "template": ServiceTemplate("GIT"), + "name": name, + }) + + if err != nil { + return nil, err + } + + return &mutation.CreateService, nil +} + +func (c *client) UploadZipToService(ctx context.Context, projectID string, serviceID string, environmentID string, zipBytes []byte) (*model.Service, error) { + url := "https://gateway.zeabur.com/projects/" + projectID + "/services/" + serviceID + "/deploy" + + method := "POST" + + var requestBody bytes.Buffer + multipartWriter := multipart.NewWriter(&requestBody) + + err := multipartWriter.WriteField("environment", environmentID) + if err != nil { + fmt.Println(err) + return nil, err + } + + fileWriter, err := multipartWriter.CreateFormFile("code", "zeabur.zip") + if err != nil { + fmt.Println(err) + return nil, err + } + _, err = io.Copy(fileWriter, bytes.NewReader(zipBytes)) + if err != nil { + fmt.Println(err) + return nil, err + } + + err = multipartWriter.Close() + if err != nil { + fmt.Println(err) + return nil, err + } + + client := &http.Client{} + + req, err := http.NewRequest(method, url, &requestBody) + if err != nil { + fmt.Println(err) + return nil, err + } + + token := viper.GetString("token") + + req.Header.Set("Content-Type", multipartWriter.FormDataContentType()) + req.Header.Set("Cookie", "token="+token) + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + return nil, err + } + defer res.Body.Close() + + return nil, nil +} diff --git a/pkg/auth/client.go b/pkg/auth/client.go index 254aaff..cd11049 100644 --- a/pkg/auth/client.go +++ b/pkg/auth/client.go @@ -47,7 +47,7 @@ func NewZeaburWebAppOAuthClient() *WebAppClient { ClientID: ZeaburOAuthCLIClientID, ClientSecret: ZeaburOAuthCLIClientSecret, RedirectURIWithoutPort: OAuthLocalServerCallbackURL, - Scopes: []string{"all"}, + Scopes: []string{"project:write", "user:write"}, AuthorizeURL: ZeaburOAuthAuthorizeURL, TokenURL: ZeaburOAuthTokenURL, From 791f928e87405c2ab922588d692fed152d33f9e2 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Wed, 20 Dec 2023 11:05:57 +0800 Subject: [PATCH 10/13] wip --- zeabur.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zeabur.json b/zeabur.json index d3c8122..df446d0 100644 --- a/zeabur.json +++ b/zeabur.json @@ -6,21 +6,21 @@ "bin": [ "zeabur.exe" ], - "hash": "5cae2460a239e20fc5bfd24632bc09895053ff35b9a669f5bb35579acd160a31" + "hash": "f5d755406af5ccad0b981f4142ded15c97e47930fc52dee6c53b3a2baeb6337c" }, "64bit": { "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_amd64.zip", "bin": [ "zeabur.exe" ], - "hash": "88264c612f0644186c87ef47828463323630091365090d4abcb2e7114614e0b7" + "hash": "670e3b54934df6c3637c313c957870d8a81af9724bb80f10c0f73fb91a89d4c6" }, "arm64": { "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_arm64.zip", "bin": [ "zeabur.exe" ], - "hash": "ba7a294c6ed76fc1f6ad7ca55be60a1471e61c1177c60a3ef5132a02a3b77b6c" + "hash": "569a2e6aa9ac2a77f0524c70471434d986c30043b3fc0003b5012b342ff98f8a" } }, "homepage": "https://zeabur.com", From f819ef3be4348898dae2199510dc5ab92a37f042 Mon Sep 17 00:00:00 2001 From: goreleaserbot Date: Fri, 1 Dec 2023 10:11:06 +0000 Subject: [PATCH 11/13] Scoop update for zeabur version v0.0.7 --- zeabur.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zeabur.json b/zeabur.json index df446d0..d3c8122 100644 --- a/zeabur.json +++ b/zeabur.json @@ -6,21 +6,21 @@ "bin": [ "zeabur.exe" ], - "hash": "f5d755406af5ccad0b981f4142ded15c97e47930fc52dee6c53b3a2baeb6337c" + "hash": "5cae2460a239e20fc5bfd24632bc09895053ff35b9a669f5bb35579acd160a31" }, "64bit": { "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_amd64.zip", "bin": [ "zeabur.exe" ], - "hash": "670e3b54934df6c3637c313c957870d8a81af9724bb80f10c0f73fb91a89d4c6" + "hash": "88264c612f0644186c87ef47828463323630091365090d4abcb2e7114614e0b7" }, "arm64": { "url": "https://github.com/zeabur/cli/releases/download/v0.0.7/zeabur_0.0.7_windows_arm64.zip", "bin": [ "zeabur.exe" ], - "hash": "569a2e6aa9ac2a77f0524c70471434d986c30043b3fc0003b5012b342ff98f8a" + "hash": "ba7a294c6ed76fc1f6ad7ca55be60a1471e61c1177c60a3ef5132a02a3b77b6c" } }, "homepage": "https://zeabur.com", From 500112249e18590c5c27d6d5b00543bb22264867 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Wed, 20 Dec 2023 11:06:55 +0800 Subject: [PATCH 12/13] fix(cli): Add CodeChallenge logic to ensure security --- go.mod | 6 +++--- pkg/auth/client.go | 2 +- pkg/webapp/webapp_flow.go | 24 ++++++++++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 72b9186..2a4dc8a 100644 --- a/go.mod +++ b/go.mod @@ -21,14 +21,16 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect ) require ( github.com/benbjohnson/clock v1.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -48,13 +50,11 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect diff --git a/pkg/auth/client.go b/pkg/auth/client.go index cd11049..d8fbeac 100644 --- a/pkg/auth/client.go +++ b/pkg/auth/client.go @@ -47,7 +47,7 @@ func NewZeaburWebAppOAuthClient() *WebAppClient { ClientID: ZeaburOAuthCLIClientID, ClientSecret: ZeaburOAuthCLIClientSecret, RedirectURIWithoutPort: OAuthLocalServerCallbackURL, - Scopes: []string{"project:write", "user:write"}, + Scopes: []string{"project:write", "user:write", "service:write", "environment:write"}, AuthorizeURL: ZeaburOAuthAuthorizeURL, TokenURL: ZeaburOAuthTokenURL, diff --git a/pkg/webapp/webapp_flow.go b/pkg/webapp/webapp_flow.go index fb0b877..cd1e9ec 100644 --- a/pkg/webapp/webapp_flow.go +++ b/pkg/webapp/webapp_flow.go @@ -5,6 +5,8 @@ package webapp import ( "context" "crypto/rand" + "crypto/sha256" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -17,8 +19,9 @@ import ( // Flow holds the state for the steps of OAuth Web Application flow. type Flow struct { - server *localServer - state string + server *localServer + state string + codeChallenge string } // InitFlow creates a new Flow instance by detecting a locally available port number. @@ -29,10 +32,12 @@ func InitFlow() (*Flow, error) { } state, _ := randomString(20) + codeChallenge, _ := randomString(43) return &Flow{ - server: server, - state: state, + server: server, + state: state, + codeChallenge: codeChallenge, }, nil } @@ -55,7 +60,9 @@ func (flow *Flow) BrowserURL(baseURL string, config oauth2.Config) (string, erro q := url.Values{} q.Set("client_id", config.ClientID) q.Set("redirect_uri", config.RedirectURL) - q.Set("scope", strings.Join(config.Scopes, " ")) + q.Set("code_challenge", genCodeChallengeS256(flow.codeChallenge)) + q.Set("code_challenge_method", "S256") + q.Set("scope", strings.Join(config.Scopes, ",")) q.Set("state", flow.state) q.Set("response_type", "code") @@ -79,7 +86,7 @@ func (flow *Flow) Wait(ctx context.Context, config oauth2.Config) (*oauth2.Token return nil, errors.New("state mismatch") } - token, err := config.Exchange(context.Background(), code.Code) + token, err := config.Exchange(context.Background(), code.Code, oauth2.SetAuthURLParam("code_verifier", flow.codeChallenge)) if err != nil { return nil, err } @@ -95,3 +102,8 @@ func randomString(length int) (string, error) { } return hex.EncodeToString(b), nil } + +func genCodeChallengeS256(s string) string { + s256 := sha256.Sum256([]byte(s)) + return base64.URLEncoding.EncodeToString(s256[:]) +} From 5202de61e7a44a84e14a5aec525fa6c3a68d49a9 Mon Sep 17 00:00:00 2001 From: michaelyuhe <2312744987@qq.com> Date: Wed, 20 Dec 2023 12:42:20 +0800 Subject: [PATCH 13/13] chore: update ci yml --- .golangci.yml | 3 +-- internal/util/pack.go | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 53bd464..f96ed80 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,8 +14,7 @@ linters: linters-settings: govet: - check-shadowing: true - enable-all: true + check-shadowing: false disable: ["fieldalignment"] errcheck: exclude-functions: diff --git a/internal/util/pack.go b/internal/util/pack.go index 9952276..03aa669 100644 --- a/internal/util/pack.go +++ b/internal/util/pack.go @@ -19,8 +19,6 @@ func PackZip() ([]byte, string, error) { return nil, "", fmt.Errorf("wrap node function: %w", err) } - os.WriteFile("zeabur.zip", zipBytes, 0644) - return zipBytes, fileName, nil }