Enhances input validation and error messages for Push command
- Improves image name validation with regex and adds default ":latest" tag if missing - Enhances port mapping and target domain validation with detailed error messages - Modifies URL availability checking logic in case of http or https protocol - Adds protocol auto-completion for ports and target domain flags inputs
- commit
- 88f6307c26b81a59a1c9f56cc4349a1b285c5dbc
- author
- bnema · · 2023-11-01 09:44 UTC
- parent
- dbdc5cd
- signature
- unsigned
2 files · +68 -24
M internal/cli/cmd/push.go +58 -24
| @@ -35,10 +35,29 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||
| 35 | 35 | imageName := args[0] | |
| 36 | 36 | fmt.Println("Exporting image:", imageName) | |
| 37 | 37 | ||
| 38 | - | // Check if the image name is valid or if a tag is specified | |
| 39 | - | match, _ := regexp.MatchString(`^([a-zA-Z0-9\-_.]+\/)?[a-zA-Z0-9\-_.]+(:[a-zA-Z0-9\-_.]+)?$`, imageName) | |
| 38 | + | // Check if the image name is valid | |
| 39 | + | match, _ := regexp.MatchString("^([a-zA-Z0-9_\\-\\.]+\\/)*[a-zA-Z0-9_\\-\\.]+(:[a-zA-Z0-9_\\-\\.]+)?$", imageName) | |
| 40 | 40 | if !match { | |
| 41 | - | fmt.Println("Invalid image name or no tag specified") | |
| 41 | + | fmt.Println("You must specify a valid image name in the form (registry/)image:tag, check your container engine image list") | |
| 42 | + | return | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // If there is :tag at the end of the image name, we append :latest | |
| 46 | + | if !strings.Contains(imageName, ":") { | |
| 47 | + | imageName += ":latest" | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Check the ports struct port:port (proto is optional) | |
| 51 | + | match, _ = regexp.MatchString("^[0-9]+:[0-9]+(\\/(tcp|udp))?$", port) | |
| 52 | + | if !match { | |
| 53 | + | fmt.Println("You must specify a port mapping in the form port:port/proto, if no protocol is specified, TCP is used") | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Check the target domain struct http(s)://domain.tld (https is optional) | |
| 58 | + | match, _ = regexp.MatchString("^(https?:\\/\\/)?([a-zA-Z0-9\\-_\\.]+\\.)+[a-zA-Z0-9\\-_\\.]+(:[0-9]+)?$", targetDomain) | |
| 59 | + | if !match { | |
| 60 | + | fmt.Println("You must specify a valid target domain in the form http(s)://domain.tld, if no protocol is specified, HTTPS is used") | |
| 42 | 61 | return | |
| 43 | 62 | } | |
| 44 | 63 | ||
| @@ -92,45 +111,60 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||
| 92 | 111 | targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes | |
| 93 | 112 | ||
| 94 | 113 | // Notify user | |
| 95 | - | fmt.Println("Wait while Traefik is setting up the domain and certificate...") | |
| 114 | + | fmt.Println("Container is running !") | |
| 115 | + | // Initialize counter for retries | |
| 116 | + | retryCount := 0 | |
| 117 | + | maxRetries := 40 | |
| 96 | 118 | progressIndicator := "" | |
| 97 | - | // Check URL availability | |
| 98 | - | for { | |
| 99 | - | client := &http.Client{ | |
| 100 | - | Timeout: time.Second * 20, | |
| 101 | - | Transport: &http.Transport{ | |
| 102 | - | TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated | |
| 103 | - | }, | |
| 119 | + | ||
| 120 | + | // Determine if the target is HTTPS or HTTP | |
| 121 | + | isHTTPS := strings.HasPrefix(targetDomain, "https://") | |
| 122 | + | ||
| 123 | + | // Initialize HTTP client | |
| 124 | + | client := &http.Client{ | |
| 125 | + | Timeout: 5 * time.Second, | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // Only set TLS config if target is HTTPS | |
| 129 | + | if isHTTPS { | |
| 130 | + | client.Transport = &http.Transport{ | |
| 131 | + | TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated | |
| 104 | 132 | } | |
| 133 | + | } | |
| 105 | 134 | ||
| 135 | + | // Check URL availability | |
| 136 | + | for { | |
| 106 | 137 | // Making GET request to the target domain | |
| 107 | - | resp, err := client.Get(targetDomain) | |
| 138 | + | _, err := client.Get(targetDomain) | |
| 108 | 139 | if err != nil { | |
| 109 | - | // we do nothing here, we just wait for the domain to be available | |
| 110 | - | ||
| 140 | + | retryCount++ | |
| 141 | + | if retryCount >= maxRetries { | |
| 142 | + | fmt.Println("\nImpossible to access the domain. Are you sure it is correct and that Traefik recognizes it?") | |
| 143 | + | break | |
| 144 | + | } | |
| 111 | 145 | } else { | |
| 112 | - | fmt.Println("Domain is available at:", targetDomain) | |
| 146 | + | fmt.Println("\nDomain is available at:", targetDomain) | |
| 113 | 147 | break | |
| 114 | 148 | } | |
| 115 | 149 | ||
| 116 | - | // Close the response body, if non-nil | |
| 117 | - | if resp != nil { | |
| 118 | - | resp.Body.Close() | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | // Update and print the progress indicator | |
| 150 | + | // Update the progress indicator | |
| 122 | 151 | progressIndicator += "." | |
| 123 | 152 | ||
| 124 | 153 | // Limit the progress indicator to 5 dots | |
| 125 | 154 | if len(progressIndicator) > 5 { | |
| 126 | - | progressIndicator = "" | |
| 155 | + | flush := "\r" | |
| 156 | + | for i := 0; i < len(progressIndicator); i++ { | |
| 157 | + | flush += " " | |
| 158 | + | } | |
| 127 | 159 | } | |
| 128 | 160 | ||
| 129 | - | fmt.Printf("\r%s", progressIndicator) | |
| 161 | + | // Use \r to move the cursor to the beginning of the line | |
| 162 | + | fmt.Printf("\r%s%s", "Wait while Traefik is setting up the domain and certificate", progressIndicator) | |
| 130 | 163 | ||
| 131 | - | // Wait for 1 secondsbefore next check | |
| 164 | + | // Wait for 1 second before the next check | |
| 132 | 165 | time.Sleep(time.Second) | |
| 133 | 166 | } | |
| 167 | + | ||
| 134 | 168 | }, | |
| 135 | 169 | } | |
| 136 | 170 | ||
| @@ -35,10 +35,29 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||||
| 35 | imageName := args[0] | 35 | imageName := args[0] | ||
| 36 | fmt.Println("Exporting image:", imageName) | 36 | fmt.Println("Exporting image:", imageName) | ||
| 37 | 37 | ||||
| 38 | - | // Check if the image name is valid or if a tag is specified | 38 | + | // Check if the image name is valid |
| 39 | - | match, _ := regexp.MatchString(`^([a-zA-Z0-9\-_.]+\/)?[a-zA-Z0-9\-_.]+(:[a-zA-Z0-9\-_.]+)?$`, imageName) | 39 | + | match, _ := regexp.MatchString("^([a-zA-Z0-9_\\-\\.]+\\/)*[a-zA-Z0-9_\\-\\.]+(:[a-zA-Z0-9_\\-\\.]+)?$", imageName) |
| 40 | if !match { | 40 | if !match { | ||
| 41 | - | fmt.Println("Invalid image name or no tag specified") | 41 | + | fmt.Println("You must specify a valid image name in the form (registry/)image:tag, check your container engine image list") |
| 42 | + | return | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // If there is :tag at the end of the image name, we append :latest | |||
| 46 | + | if !strings.Contains(imageName, ":") { | |||
| 47 | + | imageName += ":latest" | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Check the ports struct port:port (proto is optional) | |||
| 51 | + | match, _ = regexp.MatchString("^[0-9]+:[0-9]+(\\/(tcp|udp))?$", port) | |||
| 52 | + | if !match { | |||
| 53 | + | fmt.Println("You must specify a port mapping in the form port:port/proto, if no protocol is specified, TCP is used") | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // Check the target domain struct http(s)://domain.tld (https is optional) | |||
| 58 | + | match, _ = regexp.MatchString("^(https?:\\/\\/)?([a-zA-Z0-9\\-_\\.]+\\.)+[a-zA-Z0-9\\-_\\.]+(:[0-9]+)?$", targetDomain) | |||
| 59 | + | if !match { | |||
| 60 | + | fmt.Println("You must specify a valid target domain in the form http(s)://domain.tld, if no protocol is specified, HTTPS is used") | |||
| 42 | return | 61 | return | ||
| 43 | } | 62 | } | ||
| 44 | 63 | ||||
| @@ -92,45 +111,60 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||||
| 92 | targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes | 111 | targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes | ||
| 93 | 112 | ||||
| 94 | // Notify user | 113 | // Notify user | ||
| 95 | - | fmt.Println("Wait while Traefik is setting up the domain and certificate...") | 114 | + | fmt.Println("Container is running !") |
| 115 | + | // Initialize counter for retries | |||
| 116 | + | retryCount := 0 | |||
| 117 | + | maxRetries := 40 | |||
| 96 | progressIndicator := "" | 118 | progressIndicator := "" | ||
| 97 | - | // Check URL availability | 119 | + | |
| 98 | - | for { | 120 | + | // Determine if the target is HTTPS or HTTP |
| 99 | - | client := &http.Client{ | 121 | + | isHTTPS := strings.HasPrefix(targetDomain, "https://") |
| 100 | - | Timeout: time.Second * 20, | 122 | + | |
| 101 | - | Transport: &http.Transport{ | 123 | + | // Initialize HTTP client |
| 102 | - | TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated | 124 | + | client := &http.Client{ |
| 103 | - | }, | 125 | + | Timeout: 5 * time.Second, |
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // Only set TLS config if target is HTTPS | |||
| 129 | + | if isHTTPS { | |||
| 130 | + | client.Transport = &http.Transport{ | |||
| 131 | + | TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated | |||
| 104 | } | 132 | } | ||
| 133 | + | } | |||
| 105 | 134 | ||||
| 135 | + | // Check URL availability | |||
| 136 | + | for { | |||
| 106 | // Making GET request to the target domain | 137 | // Making GET request to the target domain | ||
| 107 | - | resp, err := client.Get(targetDomain) | 138 | + | _, err := client.Get(targetDomain) |
| 108 | if err != nil { | 139 | if err != nil { | ||
| 109 | - | // we do nothing here, we just wait for the domain to be available | 140 | + | retryCount++ |
| 110 | - | 141 | + | if retryCount >= maxRetries { | |
| 142 | + | fmt.Println("\nImpossible to access the domain. Are you sure it is correct and that Traefik recognizes it?") | |||
| 143 | + | break | |||
| 144 | + | } | |||
| 111 | } else { | 145 | } else { | ||
| 112 | - | fmt.Println("Domain is available at:", targetDomain) | 146 | + | fmt.Println("\nDomain is available at:", targetDomain) |
| 113 | break | 147 | break | ||
| 114 | } | 148 | } | ||
| 115 | 149 | ||||
| 116 | - | // Close the response body, if non-nil | 150 | + | // Update the progress indicator |
| 117 | - | if resp != nil { | |||
| 118 | - | resp.Body.Close() | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | // Update and print the progress indicator | |||
| 122 | progressIndicator += "." | 151 | progressIndicator += "." | ||
| 123 | 152 | ||||
| 124 | // Limit the progress indicator to 5 dots | 153 | // Limit the progress indicator to 5 dots | ||
| 125 | if len(progressIndicator) > 5 { | 154 | if len(progressIndicator) > 5 { | ||
| 126 | - | progressIndicator = "" | 155 | + | flush := "\r" |
| 156 | + | for i := 0; i < len(progressIndicator); i++ { | |||
| 157 | + | flush += " " | |||
| 158 | + | } | |||
| 127 | } | 159 | } | ||
| 128 | 160 | ||||
| 129 | - | fmt.Printf("\r%s", progressIndicator) | 161 | + | // Use \r to move the cursor to the beginning of the line |
| 162 | + | fmt.Printf("\r%s%s", "Wait while Traefik is setting up the domain and certificate", progressIndicator) | |||
| 130 | 163 | ||||
| 131 | - | // Wait for 1 secondsbefore next check | 164 | + | // Wait for 1 second before the next check |
| 132 | time.Sleep(time.Second) | 165 | time.Sleep(time.Second) | ||
| 133 | } | 166 | } | ||
| 167 | + | ||||
| 134 | }, | 168 | }, | ||
| 135 | } | 169 | } | ||
| 136 | 170 | ||||
M internal/httpserve/handler/clientendpoins.go +10 -0
| @@ -83,6 +83,16 @@ func PostPush(c echo.Context, a *server.App) error { | |||
| 83 | 83 | imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "") | |
| 84 | 84 | imageFileName = imageFileName + ".tar" | |
| 85 | 85 | ||
| 86 | + | // Check the ports struct, if there is no /tcp, or /udp, add /tcp | |
| 87 | + | if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(payload.Ports) { | |
| 88 | + | payload.Ports = payload.Ports + "/tcp" | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Check the target domain, if there is no https:// or http://, add https:// | |
| 92 | + | if !regexp.MustCompile(`^https?:\/\/`).MatchString(payload.TargetDomain) { | |
| 93 | + | payload.TargetDomain = "https://" + payload.TargetDomain | |
| 94 | + | } | |
| 95 | + | ||
| 86 | 96 | // Save the image tar in the storage | |
| 87 | 97 | imageFilePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | |
| 88 | 98 | if err != nil { | |
| @@ -83,6 +83,16 @@ func PostPush(c echo.Context, a *server.App) error { | |||||
| 83 | imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "") | 83 | imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "") | ||
| 84 | imageFileName = imageFileName + ".tar" | 84 | imageFileName = imageFileName + ".tar" | ||
| 85 | 85 | ||||
| 86 | + | // Check the ports struct, if there is no /tcp, or /udp, add /tcp | |||
| 87 | + | if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(payload.Ports) { | |||
| 88 | + | payload.Ports = payload.Ports + "/tcp" | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // Check the target domain, if there is no https:// or http://, add https:// | |||
| 92 | + | if !regexp.MustCompile(`^https?:\/\/`).MatchString(payload.TargetDomain) { | |||
| 93 | + | payload.TargetDomain = "https://" + payload.TargetDomain | |||
| 94 | + | } | |||
| 95 | + | ||||
| 86 | // Save the image tar in the storage | 96 | // Save the image tar in the storage | ||
| 87 | imageFilePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | 97 | imageFilePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | ||
| 88 | if err != nil { | 98 | if err != nil { | ||