diff --git a/cmd/cloud.go b/cmd/cloud.go index 43a02bdd..fde1e606 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -1,12 +1,9 @@ package cmd import ( - "bytes" - "encoding/json" "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "strconv" @@ -82,58 +79,20 @@ var cloudDeployCmd = &cobra.Command{ appUrl := viper.GetString("overrides.app_url") token := viper.GetString("auth.api_key") - u, err := url.Parse(apiUrl) - if err != nil { - logger.Fatal("error parsing api url: %s. %s", apiUrl, err) - } - u.Path = fmt.Sprintf("/cli/project/%s", project.ProjectId) - req, err := http.NewRequest("GET", u.String(), nil) - if err != nil { - logger.Fatal("error creating project request: %s", err) - } - req.Header.Set("Authorization", "Bearer "+token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - logger.Fatal("error requesting project: %s (%s)", err, u.String()) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - buf, _ := io.ReadAll(resp.Body) - logger.Fatal("unexpected error requesting project (%s) %s", resp.Status, string(buf)) - } - enc := json.NewDecoder(resp.Body) + client := util.NewAPIClient(apiUrl, token) + + // Get project details var projectResponse projectResponse - if err := enc.Decode(&projectResponse); err != nil { - logger.Fatal("error decoding project response json: %s", err) + if err := client.Do("GET", fmt.Sprintf("/cli/project/%s", project.ProjectId), nil, &projectResponse); err != nil { + logger.Fatal("error requesting project: %s", err) } orgId := projectResponse.Data.OrgId - // start the deployment request to get a one-time upload url - u.Path = fmt.Sprintf("/cli/deploy/start/%s/%s", orgId, project.ProjectId) - req, err = http.NewRequest("PUT", u.String(), nil) - if err != nil { - logger.Fatal("error creating url route: %s", err) - } - req.Header.Set("Authorization", "Bearer "+token) - resp, err = http.DefaultClient.Do(req) - if err != nil { - logger.Fatal("error creating start request for upload: %s", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusAccepted { - logger.Fatal("unexpected error uploading (%s)", resp.Status) - } - enc = json.NewDecoder(resp.Body) + // Start deployment var startResponse startResponse - if err := enc.Decode(&startResponse); err != nil { - logger.Fatal("error decoding start response json: %s", err) - } - resp.Body.Close() - if !startResponse.Success { - logger.Fatal("error generating start authentication: %s", startResponse.Message) + if err := client.Do("PUT", fmt.Sprintf("/cli/deploy/start/%s/%s", orgId, project.ProjectId), nil, &startResponse); err != nil { + logger.Fatal("error starting deployment: %s", err) } - logger.Debug("upload api is %s", startResponse.Data.Url) - logger.Debug("deployment id is %s", startResponse.Data.DeploymentId) // load up any gitignore files gitignore := filepath.Join(dir, ignore.Ignore) @@ -188,7 +147,7 @@ var cloudDeployCmd = &cobra.Command{ started = time.Now() // send the zip file to the upload endpoint provided - req, err = http.NewRequest("PUT", startResponse.Data.Url, of) + req, err := http.NewRequest("PUT", startResponse.Data.Url, of) if err != nil { logger.Fatal("error creating PUT request", err) } @@ -196,13 +155,12 @@ var cloudDeployCmd = &cobra.Command{ req.Header.Set("Content-Type", "application/zip") req.Header.Set("Content-Length", strconv.FormatInt(fi.Size(), 10)) - resp, err = http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { if err := updateDeploymentStatus(apiUrl, token, startResponse.Data.DeploymentId, "failed"); err != nil { logger.Fatal("%s", err) } logger.Fatal("error uploading deployment: %s", err) - } if resp.StatusCode != http.StatusOK { buf, _ := io.ReadAll(resp.Body) @@ -224,35 +182,9 @@ var cloudDeployCmd = &cobra.Command{ } func updateDeploymentStatus(apiUrl, token, deploymentId, status string) error { - u, err := url.Parse(apiUrl) - if err != nil { - return fmt.Errorf("error parsing api url: %s", err) - } - u.Path = fmt.Sprintf("/cli/deploy/upload/%s", deploymentId) - + client := util.NewAPIClient(apiUrl, token) payload := map[string]string{"state": status} - body, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("error marshalling payload: %s", err) - } - - req, err := http.NewRequest("PUT", u.String(), bytes.NewBuffer(body)) - if err != nil { - return fmt.Errorf("error creating status update request: %s", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("error sending status update request: %s", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusAccepted { - return fmt.Errorf("error updating deployment status (%s)", resp.Status) - } - return nil + return client.Do("PUT", fmt.Sprintf("/cli/deploy/upload/%s", deploymentId), payload, nil) } func init() { diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 00000000..f023fbcc --- /dev/null +++ b/cmd/env.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "fmt" + + "github.com/agentuity/cli/internal/project" + "github.com/agentuity/go-common/env" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var envCmd = &cobra.Command{ + Use: "env", + Short: "Environment related commands", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var envSetCmd = &cobra.Command{ + Use: "set [key] [value]", + Short: "Set environment variables", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + logger := env.NewLogger(cmd) + dir := resolveProjectDir(logger, cmd) + apiUrl := viper.GetString("overrides.api_url") + apiKey := viper.GetString("auth.api_key") + if apiKey == "" { + logger.Fatal("you are not logged in") + } + project := project.NewProject() + if err := project.Load(dir); err != nil { + logger.Fatal("failed to load project: %s", err) + } + _, err := project.SetProjectEnv(logger, apiUrl, apiKey, map[string]interface{}{args[0]: args[1]}) + if err != nil { + logger.Fatal("failed to set project env: %s", err) + } + printSuccess(fmt.Sprintf("Environment variable %s set successfully", args[0])) + }, +} + +var envGetCmd = &cobra.Command{ + Use: "get [key]", + Short: "Get environment variables", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logger := env.NewLogger(cmd) + dir := resolveProjectDir(logger, cmd) + apiUrl := viper.GetString("overrides.api_url") + apiKey := viper.GetString("auth.api_key") + if apiKey == "" { + logger.Fatal("you are not logged in") + } + project := project.NewProject() + if err := project.Load(dir); err != nil { + logger.Fatal("failed to load project: %s", err) + } + projectData, err := project.ListProjectEnv(logger, apiUrl, apiKey) + if err != nil { + logger.Fatal("failed to list project env: %s", err) + } + for key, value := range projectData.Env { + if key == args[0] { + fmt.Printf("%s=%s\n", key, value) + } + } + }, +} + +var envListCmd = &cobra.Command{ + Use: "list", + Short: "List all environment variables", + Run: func(cmd *cobra.Command, args []string) { + logger := env.NewLogger(cmd) + dir := resolveProjectDir(logger, cmd) + apiUrl := viper.GetString("overrides.api_url") + apiKey := viper.GetString("auth.api_key") + if apiKey == "" { + logger.Fatal("you are not logged in") + } + project := project.NewProject() + if err := project.Load(dir); err != nil { + logger.Fatal("failed to load project: %s", err) + } + projectData, err := project.ListProjectEnv(logger, apiUrl, apiKey) + if err != nil { + logger.Fatal("failed to list project env: %s", err) + } + for key, value := range projectData.Env { + fmt.Printf("%s=%s\n", key, value) + } + }, +} + +func init() { + rootCmd.AddCommand(envCmd) + envCmd.AddCommand(envSetCmd) + envCmd.AddCommand(envListCmd) + envCmd.AddCommand(envGetCmd) +} diff --git a/internal/project/project.go b/internal/project/project.go index 30486f13..8c7d735b 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -8,7 +8,9 @@ import ( "os" "path/filepath" + "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/logger" + "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/api/resource" ) @@ -24,8 +26,10 @@ type initProjectResult struct { } type ProjectData struct { - APIKey string `json:"api_key"` - ProjectId string `json:"id"` + APIKey string `json:"api_key"` + ProjectId string `json:"id"` + Env map[string]interface{} `json:"env"` + Secrets map[string]interface{} `json:"secrets"` } // InitProject will create a new project in the organization. @@ -152,6 +156,33 @@ func NewProject() *Project { return &Project{} } +type ProjectResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data ProjectData `json:"data"` +} + +func (p *Project) ListProjectEnv(logger logger.Logger, baseUrl string, token string) (*ProjectData, error) { + client := util.NewAPIClient(baseUrl, token) + + var projectResponse ProjectResponse + if err := client.Do("GET", fmt.Sprintf("/cli/project/%s", p.ProjectId), nil, &projectResponse); err != nil { + logger.Fatal("error getting project env: %s", err) + } + return &projectResponse.Data, nil +} + +func (p *Project) SetProjectEnv(logger logger.Logger, baseUrl string, token string, env map[string]interface{}) (*ProjectData, error) { + client := util.NewAPIClient(baseUrl, token) + var projectResponse ProjectResponse + if err := client.Do("PUT", fmt.Sprintf("/cli/project/%s/env", p.ProjectId), map[string]interface{}{ + "env": env, + }, &projectResponse); err != nil { + logger.Fatal("error setting project env: %s", err) + } + return &projectResponse.Data, nil +} + type DeploymentConfig struct { Provider string `yaml:"provider"` Language string `yaml:"language"` diff --git a/internal/util/api.go b/internal/util/api.go new file mode 100644 index 00000000..a67d4711 --- /dev/null +++ b/internal/util/api.go @@ -0,0 +1,63 @@ +package util + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +type APIClient struct { + baseURL string + token string + client *http.Client +} + +func NewAPIClient(baseURL, token string) *APIClient { + return &APIClient{ + baseURL: baseURL, + token: token, + client: http.DefaultClient, + } +} + +func (c *APIClient) Do(method, path string, payload interface{}, response interface{}) error { + u, err := url.Parse(c.baseURL) + if err != nil { + return fmt.Errorf("error parsing base url: %w", err) + } + u.Path = path + + var body []byte + if payload != nil { + body, err = json.Marshal(payload) + if err != nil { + return fmt.Errorf("error marshalling payload: %w", err) + } + } + + req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.token) + + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("request failed with status (%s)", resp.Status) + } + + if response != nil { + if err := json.NewDecoder(resp.Body).Decode(response); err != nil { + return fmt.Errorf("error decoding response: %w", err) + } + } + return nil +}