diff --git a/api/v1alpha1/endpointmonitor_types.go b/api/v1alpha1/endpointmonitor_types.go index 321127bd..1d03c585 100644 --- a/api/v1alpha1/endpointmonitor_types.go +++ b/api/v1alpha1/endpointmonitor_types.go @@ -113,6 +113,45 @@ type UptimeRobotConfig struct { // Defines which http status codes are treated as up or down // For ex: 200:0_401:1_503:1 (to accept 200 as down and 401 and 503 as up) CustomHTTPStatuses string `json:"customHTTPStatuses,omitempty"` + + // Optional sub-type for HTTP monitors (e.g. "HTTPS", "ping", etc.) + SubType string `json:"subType,omitempty"` + + // Optional TCP/UDP port to use when applicable (e.g. 443 for HTTPS) + Port int `json:"port,omitempty"` + + // Timeout in seconds before considering the monitored site unresponsive + Timeout int `json:"timeout,omitempty"` + + // Username for basic HTTP authentication if required by the target URL + HTTPAuthUsername string `json:"httpAuthUsername,omitempty"` + + // Password for basic HTTP authentication + HTTPAuthPassword string `json:"httpAuthPassword,omitempty"` + + // Authentication type: 1 = Basic Auth, 2 = Digest Auth + HTTPAuthType int `json:"httpAuthType,omitempty"` + + // Type of HTTP POST request: e.g., "application/x-www-form-urlencoded" + PostType string `json:"postType,omitempty"` + + // POST body value to be submitted with the request + PostValue string `json:"postValue,omitempty"` + + // HTTP method used for the check: 1 = HEAD, 2 = GET, 3 = POST, 4 = PUT, 5 = PATCH, 6 = DELETE, 7 = OPTIONS + HTTPMethod int `json:"httpMethod,omitempty"` + + // Content-Type of the HTTP POST request (e.g. "application/json") + PostContentType string `json:"postContentType,omitempty"` + + // Set of custom HTTP headers in JSON format (e.g. {"Authorization": "Bearer TOKEN"}) + CustomHTTPHeaders string `json:"customHttpHeaders,omitempty"` + + // If set to 1, SSL errors will be ignored for HTTPS monitors + IgnoreSSLErrors int `json:"ignoreSSLErrors,omitempty"` + + // If set to 1, disables expiration notifications for domains + DisableDomainExpireNotifications int `json:"disableDomainExpireNotifications,omitempty"` } // UptimeConfig defines the configuration for Uptime Monitor Provider diff --git a/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml b/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml index f7356970..c0331eb6 100644 --- a/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -365,6 +365,33 @@ spec: or down For ex: 200:0_401:1_503:1 (to accept 200 as down and 401 and 503 as up)' type: string + customHttpHeaders: + description: 'Set of custom HTTP headers in JSON format (e.g. + {"Authorization": "Bearer TOKEN"})' + type: string + disableDomainExpireNotifications: + description: If set to 1, disables expiration notifications for + domains + type: integer + httpAuthPassword: + description: Password for basic HTTP authentication + type: string + httpAuthType: + description: 'Authentication type: 1 = Basic Auth, 2 = Digest + Auth' + type: integer + httpAuthUsername: + description: Username for basic HTTP authentication if required + by the target URL + type: string + httpMethod: + description: 'HTTP method used for the check: 1 = HEAD, 2 = GET, + 3 = POST, 4 = PUT, 5 = PATCH, 6 = DELETE, 7 = OPTIONS' + type: integer + ignoreSSLErrors: + description: If set to 1, SSL errors will be ignored for HTTPS + monitors + type: integer interval: description: The uptimerobot check interval in seconds minimum: 60 @@ -390,10 +417,31 @@ spec: - http - keyword type: string + port: + description: Optional TCP/UDP port to use when applicable (e.g. + 443 for HTTPS) + type: integer + postContentType: + description: Content-Type of the HTTP POST request (e.g. "application/json") + type: string + postType: + description: 'Type of HTTP POST request: e.g., "application/x-www-form-urlencoded"' + type: string + postValue: + description: POST body value to be submitted with the request + type: string statusPages: description: The uptimerobot public status page ID to add this monitor to type: string + subType: + description: Optional sub-type for HTTP monitors (e.g. "HTTPS", + "ping", etc.) + type: string + timeout: + description: Timeout in seconds before considering the monitored + site unresponsive + type: integer type: object url: description: URL to monitor diff --git a/pkg/monitors/uptimerobot/uptime-mappers.go b/pkg/monitors/uptimerobot/uptime-mappers.go index afdec174..1b1abe86 100644 --- a/pkg/monitors/uptimerobot/uptime-mappers.go +++ b/pkg/monitors/uptimerobot/uptime-mappers.go @@ -28,6 +28,8 @@ func UptimeMonitorMonitorToBaseMonitorMapper(uptimeMonitor UptimeMonitorMonitor) providerConfig.AlertContacts = strings.Join(alertContacts, "-") } + providerConfig.HTTPMethod = uptimeMonitor.HTTPMethod + m.Config = &providerConfig return &m diff --git a/pkg/monitors/uptimerobot/uptime-monitor.go b/pkg/monitors/uptimerobot/uptime-monitor.go index 838a67d7..6b495241 100644 --- a/pkg/monitors/uptimerobot/uptime-monitor.go +++ b/pkg/monitors/uptimerobot/uptime-monitor.go @@ -48,11 +48,12 @@ func (monitor *UpTimeMonitorService) GetByName(name string) (*models.Monitor, er client := http.CreateHttpClient(monitor.url + action) - body := "api_key=" + monitor.apiKey + "&format=json&logs=1&alert_contacts=1&search=" + name + body := "api_key=" + monitor.apiKey + "&format=json&logs=1&alert_contacts=1&http_request_details=true&search=" + name response := client.PostUrlEncodedFormBody(body) - if response.StatusCode == Http.StatusOK { + switch response.StatusCode { + case Http.StatusOK: var f UptimeMonitorGetMonitorsResponse err := json.Unmarshal(response.Bytes, &f) if err != nil { @@ -68,7 +69,7 @@ func (monitor *UpTimeMonitorService) GetByName(name string) (*models.Monitor, er } return nil, nil - } else if response.StatusCode == Http.StatusTooManyRequests { + case Http.StatusTooManyRequests: log.Info("Too many requests, Monitor waiting for timeout: " + name) retryAfter := response.Header.Get("Retry-After") if retryAfter != "" { @@ -151,7 +152,8 @@ func (monitor *UpTimeMonitorService) Add(m models.Monitor) { response := client.PostUrlEncodedFormBody(body) - if response.StatusCode == Http.StatusOK { + switch response.StatusCode { + case Http.StatusOK: var f UptimeMonitorNewMonitorResponse err := json.Unmarshal(response.Bytes, &f) if err != nil { @@ -164,7 +166,7 @@ func (monitor *UpTimeMonitorService) Add(m models.Monitor) { } else { log.Info("Monitor couldn't be added: " + m.Name + ". Error: " + f.Error.Message) } - } else if response.StatusCode == Http.StatusTooManyRequests { + case Http.StatusTooManyRequests: log.Info("Too many requests, Monitor waiting for timeout: " + m.Name) retryAfter := response.Header.Get("Retry-After") if retryAfter != "" { @@ -174,7 +176,7 @@ func (monitor *UpTimeMonitorService) Add(m models.Monitor) { monitor.Add(m) // Retry after the specified delay } } - } else { + default: log.Info("AddMonitor Request failed. Status Code: " + strconv.Itoa(response.StatusCode)) } } @@ -188,7 +190,8 @@ func (monitor *UpTimeMonitorService) Update(m models.Monitor) { response := client.PostUrlEncodedFormBody(body) - if response.StatusCode == Http.StatusOK { + switch response.StatusCode { + case Http.StatusOK: var f UptimeMonitorStatusMonitorResponse err := json.Unmarshal(response.Bytes, &f) if err != nil { @@ -200,7 +203,7 @@ func (monitor *UpTimeMonitorService) Update(m models.Monitor) { } else { log.Info("Monitor couldn't be updated: " + m.Name + ". Error: " + f.Error.Message) } - } else if response.StatusCode == Http.StatusTooManyRequests { + case Http.StatusTooManyRequests: log.Info("Too many requests, Monitor waiting for timeout: " + m.Name) retryAfter := response.Header.Get("Retry-After") if retryAfter != "" { @@ -210,7 +213,7 @@ func (monitor *UpTimeMonitorService) Update(m models.Monitor) { monitor.Update(m) // Retry after the specified delay } } - } else { + default: log.Info("UpdateMonitor Request failed. Status Code: " + strconv.Itoa(response.StatusCode)) } } @@ -255,20 +258,19 @@ func (monitor *UpTimeMonitorService) processProviderConfig(m models.Monitor, cre } else if strings.Contains(strings.ToLower(providerConfig.MonitorType), "keyword") { body += "&type=2" - if providerConfig != nil && len(providerConfig.KeywordExists) != 0 { - + if len(providerConfig.KeywordExists) != 0 { if strings.Contains(strings.ToLower(providerConfig.KeywordExists), "yes") { body += "&keyword_type=1" } else if strings.Contains(strings.ToLower(providerConfig.KeywordExists), "no") { body += "&keyword_type=2" } - } else { body += "&keyword_type=1" // By default 1 (check if keyword exists) } - if providerConfig != nil && len(providerConfig.KeywordValue) != 0 { - body += "&keyword_value=" + providerConfig.KeywordValue + // Keyword Value (Required for keyword monitoring) + if len(providerConfig.KeywordValue) != 0 { + body += "&keyword_value=" + url.QueryEscape(providerConfig.KeywordValue) } else { log.Error(nil, "Monitor is of type Keyword but the `keyword-value` is missing") } @@ -276,6 +278,76 @@ func (monitor *UpTimeMonitorService) processProviderConfig(m models.Monitor, cre } else { body += "&type=1" // By default monitor is of type HTTP } + + // SubType (Optional for certain types) + if providerConfig != nil && len(providerConfig.SubType) != 0 { + body += "&sub_type=" + url.QueryEscape(providerConfig.SubType) + } + + // Port (Optional for certain types) + if providerConfig != nil && providerConfig.Port > 0 { + body += "&port=" + strconv.Itoa(providerConfig.Port) + } + + // Timeout (Optional, in seconds) + if providerConfig != nil && providerConfig.Timeout > 0 { + body += "&timeout=" + strconv.Itoa(providerConfig.Timeout) + } + + // HTTP Auth (Optional) + if providerConfig != nil && len(providerConfig.HTTPAuthUsername) != 0 && len(providerConfig.HTTPAuthPassword) != 0 { + body += "&http_username=" + url.QueryEscape(providerConfig.HTTPAuthUsername) + body += "&http_password=" + url.QueryEscape(providerConfig.HTTPAuthPassword) + if providerConfig.HTTPAuthType > 0 { + body += "&http_auth_type=" + strconv.Itoa(providerConfig.HTTPAuthType) + } + } + + // Post Type (Optional) + if providerConfig != nil && len(providerConfig.PostType) != 0 { + body += "&post_type=" + url.QueryEscape(providerConfig.PostType) + } + + // Post Value (Optional) + if providerConfig != nil && len(providerConfig.PostValue) != 0 { + body += "&post_value=" + url.QueryEscape(providerConfig.PostValue) + } + + // HTTP Method (Optional) + if providerConfig != nil && providerConfig.HTTPMethod != 0 { + body += "&http_method=" + strconv.Itoa(providerConfig.HTTPMethod) + } + + // Post Content Type (Optional) + if providerConfig != nil && len(providerConfig.PostContentType) != 0 { + body += "&post_content_type=" + url.QueryEscape(providerConfig.PostContentType) + } + + // Maintenance Windows (Optional) + if providerConfig != nil && len(providerConfig.MaintenanceWindows) != 0 { + body += "&mwindows=" + url.QueryEscape(providerConfig.MaintenanceWindows) + } + + // Custom HTTP Headers (Optional, must be sent as a JSON object) + if providerConfig != nil && len(providerConfig.CustomHTTPHeaders) != 0 { + body += "&custom_http_headers=" + url.QueryEscape(providerConfig.CustomHTTPHeaders) + } + + // Custom HTTP Statuses (Optional, must be sent in specific format) + if providerConfig != nil && len(providerConfig.CustomHTTPStatuses) != 0 { + body += "&custom_http_statuses=" + url.QueryEscape(providerConfig.CustomHTTPStatuses) + } + + // Ignore SSL Errors (Optional) + if providerConfig != nil && providerConfig.IgnoreSSLErrors > 0 { + body += "&ignore_ssl_errors=" + strconv.Itoa(providerConfig.IgnoreSSLErrors) + } + + // Disable Domain Expire Notifications (Optional) + if providerConfig != nil && providerConfig.DisableDomainExpireNotifications > 0 { + body += "&disable_domain_expire_notifications=" + strconv.Itoa(providerConfig.DisableDomainExpireNotifications) + } + return body } diff --git a/pkg/monitors/uptimerobot/uptime-monitor_test.go b/pkg/monitors/uptimerobot/uptime-monitor_test.go index cdcf13a7..7aa8e2d3 100644 --- a/pkg/monitors/uptimerobot/uptime-monitor_test.go +++ b/pkg/monitors/uptimerobot/uptime-monitor_test.go @@ -368,6 +368,35 @@ func TestAddMonitorWithMonitorType(t *testing.T) { service.Remove(*mRes) } +func TestAddMonitorWithMonitorHTTPMethod(t *testing.T) { + config := config.GetControllerConfigTest() + + service := UpTimeMonitorService{} + provider := util.GetProviderWithName(config, "UptimeRobot") + if provider == nil { + return + } + service.Setup(*provider) + + configKeyword := &endpointmonitorv1alpha1.UptimeRobotConfig{ + MonitorType: "http", + HTTPMethod: 2, // GET + } + + m := models.Monitor{Name: "google-test", URL: "https://google.com", Config: configKeyword} + service.Add(m) + + time.Sleep(time.Second * 30) + mRes, err := service.GetByName("google-test") + + if err != nil { + t.Error("Error: " + err.Error()) + } + if mRes.Name != m.Name { + t.Error("The name is incorrect, expected: " + m.Name + ", but was: " + mRes.Name) + } +} + func TestAddMonitorWithIncorrectValues(t *testing.T) { config := config.GetControllerConfigTest() diff --git a/pkg/monitors/uptimerobot/uptime-responses.go b/pkg/monitors/uptimerobot/uptime-responses.go index bd2e9d05..1342b7b2 100644 --- a/pkg/monitors/uptimerobot/uptime-responses.go +++ b/pkg/monitors/uptimerobot/uptime-responses.go @@ -13,21 +13,26 @@ type UptimeMonitorPagination struct { } type UptimeMonitorMonitor struct { - ID int `json:"id"` - FriendlyName string `json:"friendly_name"` - URL string `json:"url"` - Type int `json:"type"` - SubType string `json:"sub_type"` - KeywordType int `json:"keyword_type"` - KeywordValue string `json:"keyword_value"` - HTTPUsername string `json:"http_username"` - HTTPPassword string `json:"http_password"` - Port string `json:"port"` - Interval int `json:"interval"` - Status int `json:"status"` - CreateDatetime int `json:"create_datetime"` - Logs []UptimeMonitorLogs `json:"logs"` - AlertContacts []UptimeMonitorAlertContacts `json:"alert_contacts"` + ID int `json:"id"` + FriendlyName string `json:"friendly_name"` + URL string `json:"url"` + Type int `json:"type"` + SubType string `json:"sub_type"` + KeywordType int `json:"keyword_type"` + KeywordValue string `json:"keyword_value"` + HTTPUsername string `json:"http_username"` + HTTPPassword string `json:"http_password"` + Port string `json:"port"` + Interval int `json:"interval"` + Status int `json:"status"` + CreateDatetime int `json:"create_datetime"` + Logs []UptimeMonitorLogs `json:"logs"` + AlertContacts []UptimeMonitorAlertContacts `json:"alert_contacts"` + SSL int `json:"ssl"` + Timeout int `json:"timeout"` + CustomHTTPStatuses string `json:"custom_http_statuses"` + CustomHeader string `json:"custom_header"` + HTTPMethod int `json:"http_method"` } type UptimeMonitorAlertContacts struct { diff --git a/pkg/monitors/uptimerobot/uptime-status-page.go b/pkg/monitors/uptimerobot/uptime-status-page.go index 7c7b6a99..7542b981 100644 --- a/pkg/monitors/uptimerobot/uptime-status-page.go +++ b/pkg/monitors/uptimerobot/uptime-status-page.go @@ -163,7 +163,7 @@ func (statusPageService *UpTimeStatusPageService) RemoveMonitorFromStatusPage(st body := "api_key=" + statusPageService.apiKey + "&format=json&id=" + statusPage.ID - if existingStatusPage.Monitors != nil && len(existingStatusPage.Monitors) > 0 { + if len(existingStatusPage.Monitors) > 0 { monitors := strings.Join(existingStatusPage.Monitors, "-") body += "&monitors=" + monitors } else {