diff --git a/go/cmd/auth.go b/go/cmd/auth.go index 761098a..1aa2a10 100644 --- a/go/cmd/auth.go +++ b/go/cmd/auth.go @@ -30,7 +30,7 @@ var authLoginCmd = &cobra.Command{ if anyFlagSet { // Non-interactive: flags were explicitly provided if host == "" { - host = "cloud.soda.io" + host = "https://cloud.soda.io" } if apiKeyID == "" || apiKeySecret == "" { @@ -94,13 +94,13 @@ var authLoginCmd = &cobra.Command{ } if host == "" { - host = "cloud.soda.io" + host = "https://cloud.soda.io" } form := huh.NewForm(huh.NewGroup( huh.NewInput(). Title("Soda Cloud host"). - Description("EU: cloud.soda.io · US: cloud.us.soda.io"). + Description("EU: https://cloud.soda.io · US: https://cloud.us.soda.io"). Value(&host), huh.NewInput(). Title("API key ID"). @@ -116,13 +116,12 @@ var authLoginCmd = &cobra.Command{ } if host == "" { - host = "cloud.soda.io" + host = "https://cloud.soda.io" } - fmt.Println(output.Dim.Render(" Testing connection to " + host + "...")) - // Test connection before saving testProfile := config.Profile{Host: host, APIKeyID: apiKeyID, APIKeySecret: apiKeySecret} + fmt.Println(output.Dim.Render(" Testing connection to " + testProfile.BaseURL() + "...")) if err := api.New(testProfile).Ping(); err != nil { return err } @@ -183,13 +182,10 @@ var authStatusCmd = &cobra.Command{ return output.Errorf(2, "could not read credentials: %v", err) } p, ok := creds[profileName] - host := p.Host - if host == "" { - host = "cloud.soda.io" - } + baseURL := p.BaseURL() fmt.Printf(" %-20s %s\n", output.Bold.Render("Profile"), profileName) - fmt.Printf(" %-20s %s\n", output.Bold.Render("Host"), host) + fmt.Printf(" %-20s %s\n", output.Bold.Render("Host"), baseURL) if !ok || p.APIKeyID == "" { fmt.Printf(" %-20s %s\n", output.Bold.Render("Connection"), output.Dim.Render("not configured — run `sodacli auth login`")) @@ -218,7 +214,7 @@ var authSwitchCmd = &cobra.Command{ } func init() { - authLoginCmd.Flags().String("host", "", "Soda Cloud host (default: cloud.soda.io)") + authLoginCmd.Flags().String("host", "", "Soda Cloud host (default: https://cloud.soda.io)") authLoginCmd.Flags().String("api-key-id", "", "Soda Cloud API key ID") authLoginCmd.Flags().String("api-key-secret", "", "Soda Cloud API key secret") diff --git a/go/cmd/monitor.go b/go/cmd/monitor.go index 9c3642d..a3760e4 100644 --- a/go/cmd/monitor.go +++ b/go/cmd/monitor.go @@ -239,10 +239,18 @@ var monitorConfigCmd = &cobra.Command{ timezone, _ := cmd.Flags().GetString("timezone") - req := api.UpdateMetricMonitoringRequest{} + req := api.MetricMonitoringSettings{} if enable { t := true req.Enabled = &t + monitors := make([]api.DatasetMetricMonitorCfg, len(api.DefaultDatasetMonitorTypes)) + for i, mt := range api.DefaultDatasetMonitorTypes { + monitors[i] = api.DatasetMetricMonitorCfg{ + MetricType: mt, + Configuration: api.DatasetMonitorConfig{IsEnabled: true}, + } + } + req.DatasetMetricMonitorsConfiguration = monitors } else if disable { f := false req.Enabled = &f diff --git a/go/internal/api/client.go b/go/internal/api/client.go index 517e959..b3b6b0d 100644 --- a/go/internal/api/client.go +++ b/go/internal/api/client.go @@ -21,12 +21,8 @@ type Client struct { } func New(p config.Profile) *Client { - host := p.Host - if host == "" { - host = "cloud.soda.io" - } return &Client{ - baseURL: "https://" + host, + baseURL: p.BaseURL(), apiKeyID: p.APIKeyID, apiKeySecret: p.APIKeySecret, http: &http.Client{Timeout: 30 * time.Second}, diff --git a/go/internal/api/datasets.go b/go/internal/api/datasets.go index 793e1a0..c46aa6d 100644 --- a/go/internal/api/datasets.go +++ b/go/internal/api/datasets.go @@ -190,8 +190,7 @@ type TimePartitionRequest struct { // ── Metric monitoring (via dataset update) ──────────────────────────────────── // MetricMonitoringSettings is the shape of the `metricMonitoring` field inside -// POST /api/v1/datasets/{id}. It is separate from UpdateMetricMonitoringRequest -// which targets the (unavailable) /metricMonitoring sub-resource. +// POST /api/v1/datasets/{id}. type MetricMonitoringSettings struct { Enabled *bool `json:"enabled,omitempty"` ScanSchedule *ScanSchedule `json:"scanSchedule,omitempty"` diff --git a/go/internal/api/monitors.go b/go/internal/api/monitors.go index 38ee719..17ab084 100644 --- a/go/internal/api/monitors.go +++ b/go/internal/api/monitors.go @@ -61,12 +61,6 @@ type MetricMonitoringConfig struct { CustomSqlMetricMonitors []CustomSqlMonitor `json:"customSqlMetricMonitors"` } -type UpdateMetricMonitoringRequest struct { - Enabled *bool `json:"enabled,omitempty"` - ScanSchedule *ScanSchedule `json:"scanSchedule,omitempty"` - DatasetMetricMonitorsConfiguration []DatasetMetricMonitorCfg `json:"datasetMetricMonitorsConfiguration,omitempty"` -} - func (c *Client) GetMetricMonitoring(datasetID string) (*MetricMonitoringConfig, error) { resp, err := c.get("/api/v1/datasets/"+datasetID+"/metricMonitoring", nil) if err != nil { @@ -79,22 +73,28 @@ func (c *Client) GetMetricMonitoring(datasetID string) (*MetricMonitoringConfig, return &result, nil } -func (c *Client) UpdateMetricMonitoring(datasetID string, req UpdateMetricMonitoringRequest) (*MetricMonitoringConfig, error) { - resp, err := c.post("/api/v1/datasets/"+datasetID+"/metricMonitoring", req) +func (c *Client) UpdateMetricMonitoring(datasetID string, req MetricMonitoringSettings) (*MetricMonitoringConfig, error) { + // Use the dataset update endpoint (POST /api/v1/datasets/{id}) with the + // metricMonitoring field — the dedicated /metricMonitoring sub-resource + // is not available on all deployments. + updateReq := UpdateDatasetRequest{MetricMonitoring: &req} + resp, err := c.post("/api/v1/datasets/"+datasetID, updateReq) if err != nil { return nil, err } - var result MetricMonitoringConfig - if err := decode(resp, &result); err != nil { + // Drain and close body — the POST returns a Dataset, not MetricMonitoringConfig. + var discard Dataset + if err := decode(resp, &discard); err != nil { return nil, err } - return &result, nil + // Re-fetch the monitoring config in the expected shape. + return c.GetMetricMonitoring(datasetID) } // EnableDefaultMonitoring enables all dataset-level metric monitors for a dataset. // It uses POST /api/v1/datasets/{id} (the generic dataset update endpoint) because -// defaultDatasetMonitorTypes are the known API metricType values for dataset-level monitors. -var defaultDatasetMonitorTypes = []string{ +// DefaultDatasetMonitorTypes are the known API metricType values for dataset-level monitors. +var DefaultDatasetMonitorTypes = []string{ "rowCount", "freshness", "schema", "rowsInserted", "totalRowCountChange", "timeliness", } @@ -119,8 +119,8 @@ func (c *Client) EnableDatasetDefaults(datasetID string, monitoring, profiling b if monitoring { t := true - monitors := make([]DatasetMetricMonitorCfg, len(defaultDatasetMonitorTypes)) - for i, mt := range defaultDatasetMonitorTypes { + monitors := make([]DatasetMetricMonitorCfg, len(DefaultDatasetMonitorTypes)) + for i, mt := range DefaultDatasetMonitorTypes { monitors[i] = DatasetMetricMonitorCfg{ MetricType: mt, Configuration: DatasetMonitorConfig{IsEnabled: true}, diff --git a/go/internal/config/credentials.go b/go/internal/config/credentials.go index a895477..886cee5 100644 --- a/go/internal/config/credentials.go +++ b/go/internal/config/credentials.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "gopkg.in/yaml.v3" ) @@ -14,6 +15,20 @@ type Profile struct { APIKeySecret string `yaml:"api_key_secret"` } +// BaseURL returns the full base URL for the profile (e.g. "https://cloud.soda.io"). +// If Host already contains a scheme (http:// or https://), it is used as-is. +// Otherwise, https:// is prepended. +func (p Profile) BaseURL() string { + host := p.Host + if host == "" { + host = "https://cloud.soda.io" + } + if strings.HasPrefix(host, "http://") || strings.HasPrefix(host, "https://") { + return strings.TrimRight(host, "/") + } + return "https://" + strings.TrimRight(host, "/") +} + type Credentials map[string]Profile func CredentialsPath() (string, error) {