Skip to content

Commit

Permalink
feat: world forge organization and project
Browse files Browse the repository at this point in the history
  • Loading branch information
zulkhair committed Jan 2, 2025
1 parent caa3fc3 commit 09dbde2
Show file tree
Hide file tree
Showing 14 changed files with 2,748 additions and 43 deletions.
148 changes: 148 additions & 0 deletions cmd/world/forge/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package forge

import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"strings"

"github.com/google/uuid"
"github.com/rotisserie/eris"
"github.com/tidwall/gjson"

"pkg.world.dev/world-cli/common/globalconfig"
)

// sendRequest sends an HTTP request with auth token and returns the response body
func sendRequest(ctx context.Context, method, url string, body interface{}) ([]byte, error) {
var bodyReader io.Reader

if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, eris.Wrap(err, "Failed to marshal request body")
}
bodyReader = bytes.NewReader(jsonBody)
}

// Get credential from config
cred, err := globalconfig.GetGlobalConfig()
if err != nil {
return nil, eris.Wrap(err, "Failed to get credential")
}

// Create request
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return nil, eris.Wrap(err, "Failed to create request")
}

// Add authorization header
req.Header.Add("Authorization", "Bearer "+cred.Credential.Token)

// Add content-type header for requests with body
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

// Make request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, eris.Wrap(err, "Failed to make request")
}
defer resp.Body.Close()

// Check status code
if resp.StatusCode != http.StatusOK {
return nil, eris.Errorf("Unexpected status code: %d", resp.StatusCode)
}

// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, eris.Wrap(err, "Failed to read response body")
}

return respBody, nil
}

func parseResponse[T any](body []byte) (*T, error) {
result := gjson.GetBytes(body, "data")
if !result.Exists() {
return nil, eris.New("Missing data field in response")
}

var data T
if err := json.Unmarshal([]byte(result.Raw), &data); err != nil {
return nil, eris.Wrap(err, "Failed to parse response")
}

return &data, nil
}

func printNoSelectedOrganization() {
fmt.Println("You don't have any organization selected.")
fmt.Println("Use 'world forge organization switch' to select one.")
fmt.Println()
}

func printNoSelectedProject() {
fmt.Println("You don't have any project selected.")
fmt.Println("Use 'world forge project switch' to select one.")
fmt.Println()
}

func printNoProjectsInOrganization() {
fmt.Println("You don't have any projects in this organization yet.")
fmt.Println("Use 'world forge project create' to create one.")
fmt.Println()
}

func isAlphanumeric(s string) bool {
for _, r := range s {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
return false
}
}
return true
}

var generateKey = func() string {

Check failure on line 118 in cmd/world/forge/common.go

View workflow job for this annotation

GitHub Actions / Go

var must not be placed after func (desired order: const,var,type,func) (decorder)
return strings.ReplaceAll(uuid.NewString(), "-", "")
}

// Change from function to variable
var openBrowser = func(url string) error {

Check failure on line 123 in cmd/world/forge/common.go

View workflow job for this annotation

GitHub Actions / Go

var must not be placed after func (desired order: const,var,type,func) (decorder)
var err error

Check failure on line 124 in cmd/world/forge/common.go

View workflow job for this annotation

GitHub Actions / Go

var must not be placed after func (desired order: const,var,type,func) (decorder)
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = eris.New("unsupported platform")
}
if err != nil {
return eris.Wrap(err, "failed to open browser")
}
return nil
}

var getInput = func() (string, error) {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return "", eris.Wrap(err, "Failed to read input")
}
return strings.TrimSpace(input), nil
}
130 changes: 130 additions & 0 deletions cmd/world/forge/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package forge

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/rotisserie/eris"

"pkg.world.dev/world-cli/common/globalconfig"
)

// Deploy a project
func deploy(ctx context.Context) error {
globalConfig, err := globalconfig.GetGlobalConfig()
if err != nil {
return eris.Wrap(err, "Failed to get global config")
}

projectID := globalConfig.ProjectID
organizationID := globalConfig.OrganizationID

if organizationID == "" {
printNoSelectedOrganization()
return nil
}

if projectID == "" {
printNoSelectedProject()
return nil
}

// Get organization details
org, err := getSelectedOrganization(ctx)
if err != nil {
return eris.Wrap(err, "Failed to get organization details")
}

// Get project details
prj, err := getSelectedProject(ctx)
if err != nil {
return eris.Wrap(err, "Failed to get project details")
}

fmt.Println("Deployment Details")
fmt.Println("-----------------")
fmt.Printf("Organization: %s\n", org.Name)
fmt.Printf("Org Slug: %s\n", org.Slug)
fmt.Printf("Project: %s\n", prj.Name)
fmt.Printf("Project Slug: %s\n", prj.Slug)
fmt.Printf("Repository: %s\n\n", prj.RepoURL)

deployURL := fmt.Sprintf("%s/api/organization/%s/project/%s/deploy", baseURL, organizationID, projectID)
_, err = sendRequest(ctx, http.MethodPost, deployURL, nil)
if err != nil {
return eris.Wrap(err, "Failed to deploy project")
}

fmt.Println("\n✨ Your deployment is being processed! ✨")
fmt.Println("\nTo check the status of your deployment, run:")
fmt.Println(" $ 'world forge deployment status'")

return nil
}

// Destroy a project
func destroy(ctx context.Context) error {
globalConfig, err := globalconfig.GetGlobalConfig()
if err != nil {
return eris.Wrap(err, "Failed to get global config")
}

projectID := globalConfig.ProjectID
organizationID := globalConfig.OrganizationID

if organizationID == "" {
printNoSelectedOrganization()
return nil
}

if projectID == "" {
printNoSelectedProject()
return nil
}

// Get organization details
org, err := getSelectedOrganization(ctx)
if err != nil {
return eris.Wrap(err, "Failed to get organization details")
}

// Get project details
prj, err := getSelectedProject(ctx)
if err != nil {
return eris.Wrap(err, "Failed to get project details")
}

fmt.Println("Project Details")
fmt.Println("-----------------")
fmt.Printf("Organization: %s\n", org.Name)
fmt.Printf("Org Slug: %s\n", org.Slug)
fmt.Printf("Project: %s\n", prj.Name)
fmt.Printf("Project Slug: %s\n", prj.Slug)
fmt.Printf("Repository: %s\n\n", prj.RepoURL)

fmt.Print("Are you sure you want to destroy this project? (y/N): ")
response, err := getInput()
if err != nil {
return eris.Wrap(err, "Failed to read response")
}

response = strings.ToLower(strings.TrimSpace(response))
if response != "y" {
fmt.Println("Destroy cancelled")
return nil
}

destroyURL := fmt.Sprintf("%s/api/organization/%s/project/%s/destroy", baseURL, organizationID, projectID)
_, err = sendRequest(ctx, http.MethodPost, destroyURL, nil)
if err != nil {
return eris.Wrap(err, "Failed to destroy project")
}

fmt.Println("\n🗑️ Your destroy request is being processed!")
fmt.Println("\nTo check the status of your destroy request, run:")
fmt.Println(" $ 'world forge deployment status'")

return nil
}
Loading

0 comments on commit 09dbde2

Please sign in to comment.