diff --git a/hossted/request.go b/hossted/request.go index 1d34a4e..7f9c88c 100644 --- a/hossted/request.go +++ b/hossted/request.go @@ -3,7 +3,7 @@ package hossted import ( "crypto/tls" "fmt" - "io/ioutil" + "io" "net/http" "net/url" ) @@ -54,7 +54,7 @@ func (h *HosstedRequest) SendRequest() (string, error) { } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", err } diff --git a/hossted/service/common/common.go b/hossted/service/common/common.go index 06da590..f812be0 100644 --- a/hossted/service/common/common.go +++ b/hossted/service/common/common.go @@ -61,10 +61,6 @@ func HttpRequest(method, url, token string, body []byte) error { Message interface{} `json:"message"` } - if resp.StatusCode != 200 { - return fmt.Errorf("rrror sending event, errcode: %d", resp.StatusCode) - } - respBody, err := io.ReadAll(resp.Body) if err != nil { return err @@ -76,6 +72,10 @@ func HttpRequest(method, url, token string, body []byte) error { return err } + if resp.StatusCode != 200 { + return fmt.Errorf("error sending event, errcode: %d, msg: %s", resp.StatusCode, apiResponse.Message) + } + if !apiResponse.Success { return fmt.Errorf("api response indicates failure: %v", apiResponse) } diff --git a/hossted/service/compose/reconcile_compose.go b/hossted/service/compose/reconcile_compose.go index 6da7c10..850c9a2 100644 --- a/hossted/service/compose/reconcile_compose.go +++ b/hossted/service/compose/reconcile_compose.go @@ -178,10 +178,10 @@ func writeComposeRequest2File( return isComposeStateChange, nil } -type optionsState struct { - Monitoring bool `json:"monitoring"` - Logging bool `json:"logging"` - CVEScan bool `json:"cve_scan"` +type OptionsState struct { + Monitoring bool `json:"monitoring,omitempty"` + Logging bool `json:"logging,omitempty"` + CVEScan bool `json:"cve_scan,omitempty"` } type URLInfo struct { @@ -194,7 +194,7 @@ type AccessInfo struct { URLs []URLInfo `json:"urls"` } -type request struct { +type Request struct { UUID string `json:"uuid"` OsUUID string `json:"osuuid"` OrgID string `json:"org_id"` @@ -203,7 +203,7 @@ type request struct { Product string `json:"product,omitempty"` CPUNum string `json:"cpunum,omitempty"` Memory string `json:"memory,omitempty"` - OptionsState optionsState `json:"options_state"` + OptionsState OptionsState `json:"options_state,omitempty"` ComposeFile string `json:"compose_file,omitempty"` AccessInfo AccessInfo `json:"access_info,omitempty"` } @@ -238,7 +238,7 @@ func sendComposeInfo(appFilePath string, osInfo OsInfo) error { return err } - accessInfo := getAccessInfo("/opt/" + projectName + "/.env") + accessInfo := GetAccessInfo("/opt/" + projectName + "/.env") var data map[string]AppRequest if err = json.Unmarshal(composeInfo, &data); err != nil { @@ -276,7 +276,7 @@ func sendComposeInfo(appFilePath string, osInfo OsInfo) error { // registerApplications registers all applications with the specified API URL. func registerApplications(data map[string]AppRequest, osInfo OsInfo, accessInfo AccessInfo, cpu, mem, orgID, token, composeUrl string) error { for appName, compose := range data { - newReq := request{ + newReq := Request{ UUID: compose.AppAPIInfo.AppUUID, OsUUID: compose.AppAPIInfo.OsUUID, Email: compose.AppAPIInfo.EmailID, @@ -286,7 +286,7 @@ func registerApplications(data map[string]AppRequest, osInfo OsInfo, accessInfo Product: appName, CPUNum: cpu, Memory: mem, - OptionsState: optionsState{ + OptionsState: OptionsState{ Monitoring: true, Logging: true, CVEScan: true, @@ -294,7 +294,7 @@ func registerApplications(data map[string]AppRequest, osInfo OsInfo, accessInfo ComposeFile: compose.AppInfo.ComposeFile, } - if err := sendRequest("POST", composeUrl, token, newReq); err != nil { + if err := SendRequest("POST", composeUrl, token, newReq); err != nil { return err } fmt.Printf("Successfully registered app [%s] with appID [%s]\n", appName, compose.AppAPIInfo.AppUUID) @@ -325,7 +325,7 @@ func registerDockerInstances(data map[string]AppRequest, osInfo OsInfo, token, c CreatedAt: ci.CreatedAt, } - if err := sendRequest("POST", containersUrl, token, newDI); err != nil { + if err := SendRequest("POST", containersUrl, token, newDI); err != nil { return err } @@ -345,7 +345,7 @@ func submitPatchRequest(osInfo OsInfo, compose map[string]AppRequest, accessInfo applicationName = appName } - newReq := request{ + newReq := Request{ UUID: osInfo.AppUUID, OsUUID: osInfo.OsUUID, AccessInfo: accessInfo, @@ -354,7 +354,7 @@ func submitPatchRequest(osInfo OsInfo, compose map[string]AppRequest, accessInfo Product: applicationName, CPUNum: cpu, Memory: mem, - OptionsState: optionsState{ + OptionsState: OptionsState{ Monitoring: true, Logging: true, CVEScan: false, @@ -362,11 +362,11 @@ func submitPatchRequest(osInfo OsInfo, compose map[string]AppRequest, accessInfo ComposeFile: composeFile, } - return sendRequest(http.MethodPatch, composeUrl, osInfo.Token, newReq) + return SendRequest(http.MethodPatch, composeUrl, osInfo.Token, newReq) } // sendRequest handles HTTP requests for a given method, URL, token, and request body. -func sendRequest(method, url, token string, reqBody interface{}) error { +func SendRequest(method, url, token string, reqBody interface{}) error { body, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("failed to marshal JSON: %v", err) @@ -705,7 +705,7 @@ func runMonitoringCompose(monitoringEnable, osUUID, appUUID string) error { return nil } -func getAccessInfo(filepath string) *AccessInfo { +func GetAccessInfo(filepath string) *AccessInfo { file, err := os.Open(filepath) if err != nil { return &AccessInfo{} diff --git a/hossted/setDomain.go b/hossted/setDomain.go index e0572ef..f718557 100644 --- a/hossted/setDomain.go +++ b/hossted/setDomain.go @@ -5,12 +5,14 @@ import ( "errors" "fmt" "log" + "net/http" "os" "os/exec" "path/filepath" "regexp" "strings" + "github.com/hossted/cli/hossted/service/compose" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -82,6 +84,12 @@ func SetDomain(env, app, domain string) error { fmt.Println("Hossted Platform Domain set successfully") } else { + + err = CheckHosstedAuthFiles() + if err != nil { + fmt.Println("Please run hossted activate -t compose, to activate the vm") + os.Exit(1) + } if !HasContainerRunning() { fmt.Println("The application still in configuration") os.Exit(0) @@ -94,23 +102,18 @@ func SetDomain(env, app, domain string) error { return fmt.Errorf("\n\n%w", err) } - err = AddDomainToMotd(domain) + err = ChangeMOTD(domain) if err != nil { return err } check := verifyInputFormat(domain, "domain") if !check { - return fmt.Errorf("Invalid domain input. Expecting domain name (e.g. example.com).\nInput - %s\n", domain) + return fmt.Errorf("invalid domain input. Expecting domain name (e.g. example.com). input - %s", domain) } - // Get .env file and appDir - appConfig, err := config.GetAppConfig(app) - if err != nil { - return err - } - appDir := appConfig.AppPath - envPath, err := getAppFilePath(appConfig.AppPath, ".env") + appDir := "/opt/" + app + envPath, err := getAppFilePath(appDir, ".env") if err != nil { return err } @@ -149,45 +152,143 @@ func SetDomain(env, app, domain string) error { typeActivity := "set_domain" sendActivityLog(env, uuid, fullCommand, options, typeActivity) + + osInfo, err := compose.GetClusterInfo() + if err != nil { + return fmt.Errorf("error getting cluster info %s", err) + } + + projectName, err := getProjectName() + if err != nil { + return fmt.Errorf("error getting project name %s", err) + } + + accessInfo := compose.GetAccessInfo("/opt/" + projectName + "/.env") + + err = submitPatchRequest(osInfo, *accessInfo) + if err != nil { + return fmt.Errorf("error submitting patch request %v", err) + } + return nil } return nil } -// ChangeMOTD changes the content of the MOTD file, to match the set domain changes -// TODO: print status -// TODO: Allow domain to be something other than .com by changing the regex patten -func ChangeMOTD(domain string) error { +// submitPatchRequest sends a PATCH request with VM info for marketplace setups. +func submitPatchRequest(osInfo compose.OsInfo, accessInfo compose.AccessInfo) error { + composeUrl := osInfo.HosstedApiUrl + "/compose/hosts/" + osInfo.OsUUID + + type req struct { + UUID string `json:"uuid"` // Application UUID + OsUUID string `json:"osuuid"` // Operating System UUID + AccessInfo compose.AccessInfo `json:"access_info"` // Access information for the VM + Type string `json:"type"` // Type of the request, e.g., "vm" + } + newReq := req{ + UUID: osInfo.AppUUID, + OsUUID: osInfo.OsUUID, + AccessInfo: accessInfo, + Type: "vm", + } + + return compose.SendRequest(http.MethodPatch, composeUrl, osInfo.Token, newReq) +} + +func ChangeMOTD(domain string) error { filepath := "/etc/motd" + + // Read the file b, err := readProtected(filepath) if err != nil { - return fmt.Errorf("Can't read the /etc/motd file. Please check - %s and contact administrator.\n%w\n", filepath, err) + return fmt.Errorf("unable to read the /etc/motd file. Please check %s and contact administrator: %w", filepath, err) } content := string(b) - // Currently only .com is supported. Looking for line like - // Your ^[[01;32mgitbucket^[[0m is available under ^[[01;34m http://3.215.23.221.c.hossted.com ^[[0m - re, err := regexp.Compile(`(.*available under\s*.*https?:\/\/)(.*\.com)(.*)`) + // Match and update any URL that starts with https:// followed by a domain + re := regexp.MustCompile(`https://[\w\.\-]+\.\w+`) + updatedContent := re.ReplaceAllString(content, fmt.Sprintf("https://%s", domain)) + + if updatedContent == content { + return errors.New("no matching pattern found in /etc/motd. Please ensure the content is formatted correctly") + } + + // Write the updated content back to the file + err = writeProtected(filepath, []byte(updatedContent)) if err != nil { - return err + return fmt.Errorf("failed to write to the /etc/motd file: %w", err) } - matches := re.FindAllStringSubmatch(content, -1) - if len(matches) > 0 { - if len(matches[0]) == 4 { - new := matches[0][1] + domain + matches[0][3] - content = strings.Replace(content, matches[0][0], new, 1) // Replace the containing new with new string - } + return nil +} + +// CheckHosstedAuthFiles checks if the files ~/.hossted/auth.json and ~/.hossted/authresp.json exist. +func CheckHosstedAuthFiles() error { + // Get the home directory + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + + // Define the file paths + authFilePath := filepath.Join(homeDir, ".hossted", "auth.json") + authRespFilePath := filepath.Join(homeDir, ".hossted", "authresp.json") + + // Check if auth.json exists + if _, err := os.Stat(authFilePath); os.IsNotExist(err) { + return fmt.Errorf("file %s does not exist", authFilePath) + } + + // Check if authresp.json exists + if _, err := os.Stat(authRespFilePath); os.IsNotExist(err) { + return fmt.Errorf("file %s does not exist", authRespFilePath) + } + + // Both files exist + return nil +} + +func getSoftwarePath() (string, error) { + path := "/opt/hossted/run/software.txt" + if _, err := os.Stat(path); os.IsNotExist(err) { + return "", nil } else { - return errors.New("No matching pattern in /etc/motd. Please check.\n") + return path, nil } - // Write back to file - err = writeProtected(filepath, []byte(content)) +} + +func getProjectName() (string, error) { + path, err := getSoftwarePath() if err != nil { - return err + fmt.Println("Error getting software path", err) } - return nil + // its a market place VM, access info object will exist + if path == "/opt/hossted/run/software.txt" { + // read the file in this path + // file will have this convention - Linnovate-AWS-keycloak + // capture the last word ie keycloak in this case. + // and use this last work ie instead of osInfo.ProjectName + data, err := os.ReadFile(path) + if err != nil { + fmt.Println("Error reading file:", err) + return "", err + } + + // The file will have the convention Linnovate-AWS-keycloak + // Capture the last word (i.e., keycloak in this case) + softwareName := strings.TrimSpace(string(data)) + words := strings.Split(softwareName, "-") + if len(words) > 0 { + projectName := words[len(words)-1] + // Use this last word (i.e., keycloak) instead of osInfo.ProjectName + return projectName, nil + } + } else if path == "" { + fmt.Println("Contact Hossted support to add Access Info object") + return "", nil + } + return "", nil } diff --git a/hossted/structMethod.go b/hossted/structMethod.go index 884d373..ba433fe 100644 --- a/hossted/structMethod.go +++ b/hossted/structMethod.go @@ -18,7 +18,7 @@ func (c *Config) GetAppConfig(in string) (ConfigApplication, error) { } // Check if any matched if ca.AppName == "" { - return ca, fmt.Errorf("No Config found for app - %s", in) + return ca, fmt.Errorf("no config found for app - %s", in) } return ca, nil } diff --git a/hossted/utils.go b/hossted/utils.go index 92eaad9..d2cf604 100644 --- a/hossted/utils.go +++ b/hossted/utils.go @@ -371,7 +371,7 @@ func writeProtected(path string, b []byte) error { func getAppFilePath(base, relative string) (string, error) { path := filepath.Join(base, relative) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { - return "", fmt.Errorf("File not exists. Please check. %w", err) + return "", fmt.Errorf("file not exists. please check. %w", err) } return path, nil } @@ -385,24 +385,37 @@ func PrettyPrint(i interface{}) string { func stopTraefik(appDir string) error { fmt.Println("Stopping traefik...") - command := []string{"sudo docker compose down"} - err, _, stderr := Shell(appDir, command) + // Construct the docker compose down command + cmd := exec.Command("sudo", "docker", "compose", "down") + cmd.Dir = appDir // Set the working directory for the command + + // Run the command and capture stdout and stderr + output, err := cmd.CombinedOutput() if err != nil { - return err + return fmt.Errorf("error stopping traefik: %v\nOutput: %s", err, string(output)) } - fmt.Println(trimOutput(stderr)) - fmt.Println("traefik stopeed") + + // Print the trimmed output for debugging + fmt.Println(string(output)) + fmt.Println("Traefik stopped") return nil } + func dockerUp(appDir string) error { fmt.Println("Restarting service...") - command := []string{"sudo docker compose up -d"} - err, _, stderr := Shell(appDir, command) + // Construct the docker compose command + cmd := exec.Command("sudo", "docker", "compose", "up", "-d") + cmd.Dir = appDir // Set the working directory for the command + + // Run the command and capture stdout and stderr + output, err := cmd.CombinedOutput() if err != nil { - return err + return fmt.Errorf("error running docker compose: %v\nOutput: %s", err, string(output)) } - fmt.Println(trimOutput(stderr)) + + // Print the trimmed output for debugging + fmt.Println(string(output)) return nil } @@ -575,7 +588,7 @@ func sendActivityLog(env, uuid, fullCommand, options, typeActivity string) (acti user, err := user.Current() if err != nil { - log.Fatalf(err.Error()) + log.Fatalf("%s", err.Error()) } userName := user.Username @@ -599,16 +612,14 @@ func sendActivityLog(env, uuid, fullCommand, options, typeActivity string) (acti resp, err := req.SendRequest() if err != nil { - fmt.Println("err", err) return response, err } err = json.Unmarshal([]byte(resp), &response) if err != nil { - return response, fmt.Errorf("Failed to parse JSON. %w", err) + return response, fmt.Errorf("failed to parse JSON. %w", err) } - //fmt.Printf("%v \n", response.Message) return response, nil }