Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ func (c *Client) GetIncidentByID(ctx context.Context, id string) (*Incident, err
}

// CreateIncident creates a new incident using raw HTTP POST.
func (c *Client) CreateIncident(ctx context.Context, title string, opts map[string]string) (*Incident, error) {
func (c *Client) CreateIncident(ctx context.Context, title string, opts map[string]interface{}) (*Incident, error) {
// Build JSON:API request body
requestBody := map[string]interface{}{
"data": map[string]interface{}{
Expand All @@ -1097,6 +1097,11 @@ func (c *Client) CreateIncident(ctx context.Context, title string, opts map[stri
if status, ok := opts["status"]; ok {
attributes["status"] = status
}
for _, key := range []string{"service_ids", "incident_type_ids", "functionality_ids", "environment_ids", "group_ids", "cause_ids"} {
if value, ok := opts[key]; ok {
attributes[key] = value
}
}

bodyBytes, err := json.Marshal(requestBody)
if err != nil {
Expand Down Expand Up @@ -1150,7 +1155,7 @@ func (c *Client) CreateIncident(ctx context.Context, title string, opts map[stri
}

// UpdateIncident updates an incident using raw HTTP PUT.
func (c *Client) UpdateIncident(ctx context.Context, id string, opts map[string]string) (*Incident, error) {
func (c *Client) UpdateIncident(ctx context.Context, id string, opts map[string]interface{}) (*Incident, error) {
// Build JSON:API request body with only changed attributes
attributes := make(map[string]interface{})
if title, ok := opts["title"]; ok {
Expand All @@ -1165,6 +1170,11 @@ func (c *Client) UpdateIncident(ctx context.Context, id string, opts map[string]
if status, ok := opts["status"]; ok {
attributes["status"] = status
}
for _, key := range []string{"service_ids", "incident_type_ids", "functionality_ids", "environment_ids", "group_ids", "cause_ids"} {
if value, ok := opts[key]; ok {
attributes[key] = value
}
}

requestBody := map[string]interface{}{
"data": map[string]interface{}{
Expand Down
32 changes: 29 additions & 3 deletions internal/api/client_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,26 @@ func TestCreateIncident(t *testing.T) {
t.Errorf("expected POST, got %s", r.Method)
}

var requestBody map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
data := requestBody["data"].(map[string]interface{})
attrs := data["attributes"].(map[string]interface{})
for key, want := range map[string]string{
"service_ids": "api-gateway",
"incident_type_ids": "customer-impacting",
"functionality_ids": "checkout",
"environment_ids": "production",
"group_ids": "platform",
"cause_ids": "deployment",
} {
values := attrs[key].([]interface{})
if len(values) == 0 || values[0] != want {
t.Errorf("request %s = %v, want first value %s", key, values, want)
}
}

w.Header().Set("Content-Type", "application/vnd.api+json")
w.WriteHeader(http.StatusCreated)
resp := map[string]interface{}{
Expand All @@ -275,9 +295,15 @@ func TestCreateIncident(t *testing.T) {
defer server.Close()

client := newTestClient(t, server.URL)
inc, err := client.CreateIncident(context.Background(), "New Incident", map[string]string{
"summary": "Test summary",
"status": "started",
inc, err := client.CreateIncident(context.Background(), "New Incident", map[string]interface{}{
"summary": "Test summary",
"status": "started",
"service_ids": []string{"api-gateway", "payments"},
"incident_type_ids": []string{"customer-impacting"},
"functionality_ids": []string{"checkout"},
"environment_ids": []string{"production"},
"group_ids": []string{"platform"},
"cause_ids": []string{"deployment"},
})
if err != nil {
t.Fatalf("CreateIncident returned error: %v", err)
Expand Down
35 changes: 30 additions & 5 deletions internal/api/client_mutations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ func TestUpdateIncident(t *testing.T) {
defer server.Close()

client := newTestClient(t, server.URL)
inc, err := client.UpdateIncident(context.Background(), "inc-1", map[string]string{
"title": "Updated Title",
"status": "mitigated",
inc, err := client.UpdateIncident(context.Background(), "inc-1", map[string]interface{}{
"title": "Updated Title",
"status": "mitigated",
"service_ids": []string{"api-gateway", "payments"},
"incident_type_ids": []string{"customer-impacting"},
"functionality_ids": []string{"checkout"},
"environment_ids": []string{"production"},
"group_ids": []string{"platform"},
"cause_ids": []string{"deployment"},
})
if err != nil {
t.Fatalf("UpdateIncident returned error: %v", err)
Expand All @@ -70,6 +76,25 @@ func TestUpdateIncident(t *testing.T) {
if attrs["status"] != "mitigated" {
t.Errorf("request status = %q, want %q", attrs["status"], "mitigated")
}
serviceIDs := attrs["service_ids"].([]interface{})
if len(serviceIDs) != 2 || serviceIDs[0] != "api-gateway" || serviceIDs[1] != "payments" {
t.Errorf("request service_ids = %v, want [api-gateway payments]", serviceIDs)
}
incidentTypeIDs := attrs["incident_type_ids"].([]interface{})
if len(incidentTypeIDs) != 1 || incidentTypeIDs[0] != "customer-impacting" {
t.Errorf("request incident_type_ids = %v, want [customer-impacting]", incidentTypeIDs)
}
for key, want := range map[string]string{
"functionality_ids": "checkout",
"environment_ids": "production",
"group_ids": "platform",
"cause_ids": "deployment",
} {
values := attrs[key].([]interface{})
if len(values) != 1 || values[0] != want {
t.Errorf("request %s = %v, want [%s]", key, values, want)
}
}
}

func TestUpdateIncidentNotFound(t *testing.T) {
Expand All @@ -79,7 +104,7 @@ func TestUpdateIncidentNotFound(t *testing.T) {
defer server.Close()

client := newTestClient(t, server.URL)
_, err := client.UpdateIncident(context.Background(), "nonexistent", map[string]string{"title": "x"})
_, err := client.UpdateIncident(context.Background(), "nonexistent", map[string]interface{}{"title": "x"})
if err == nil {
t.Fatal("expected error for 404")
}
Expand All @@ -95,7 +120,7 @@ func TestUpdateIncidentForbidden(t *testing.T) {
defer server.Close()

client := newTestClient(t, server.URL)
_, err := client.UpdateIncident(context.Background(), "inc-1", map[string]string{"title": "x"})
_, err := client.UpdateIncident(context.Background(), "inc-1", map[string]interface{}{"title": "x"})
if err == nil {
t.Fatal("expected error for 403")
}
Expand Down
67 changes: 67 additions & 0 deletions internal/cmd/incidents/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package incidents

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
Expand Down Expand Up @@ -287,6 +288,12 @@ func TestRunCreateTable(t *testing.T) {
cmd.Flags().String("summary", "Test summary", "")
cmd.Flags().String("severity", "", "")
cmd.Flags().String("status", "started", "")
cmd.Flags().StringSlice("services", []string{"api-gateway", "payments"}, "")
cmd.Flags().StringSlice("types", []string{"customer-impacting"}, "")
cmd.Flags().StringSlice("functionalities", []string{"checkout"}, "")
cmd.Flags().StringSlice("environments", []string{"production"}, "")
cmd.Flags().StringSlice("teams", []string{"platform"}, "")
cmd.Flags().StringSlice("causes", []string{"deployment"}, "")

output := captureStdout(t, func() {
err := runCreate(cmd, nil)
Expand Down Expand Up @@ -315,6 +322,12 @@ func TestRunCreateJSON(t *testing.T) {
cmd.Flags().String("summary", "", "")
cmd.Flags().String("severity", "", "")
cmd.Flags().String("status", "", "")
cmd.Flags().StringSlice("services", nil, "")
cmd.Flags().StringSlice("types", nil, "")
cmd.Flags().StringSlice("functionalities", nil, "")
cmd.Flags().StringSlice("environments", nil, "")
cmd.Flags().StringSlice("teams", nil, "")
cmd.Flags().StringSlice("causes", nil, "")

output := captureStdout(t, func() {
err := runCreate(cmd, nil)
Expand Down Expand Up @@ -344,6 +357,12 @@ func TestRunUpdateTable(t *testing.T) {
cmd.Flags().String("summary", "", "")
cmd.Flags().String("severity", "", "")
cmd.Flags().String("status", "", "")
cmd.Flags().StringSlice("services", nil, "")
cmd.Flags().StringSlice("types", nil, "")
cmd.Flags().StringSlice("functionalities", nil, "")
cmd.Flags().StringSlice("environments", nil, "")
cmd.Flags().StringSlice("teams", nil, "")
cmd.Flags().StringSlice("causes", nil, "")

// Simulate the user passing --status=mitigated
cmd.Flags().Set("status", "mitigated")
Expand All @@ -370,6 +389,12 @@ func TestRunUpdateNoFlags(t *testing.T) {
cmd.Flags().String("summary", "", "")
cmd.Flags().String("severity", "", "")
cmd.Flags().String("status", "", "")
cmd.Flags().StringSlice("services", nil, "")
cmd.Flags().StringSlice("types", nil, "")
cmd.Flags().StringSlice("functionalities", nil, "")
cmd.Flags().StringSlice("environments", nil, "")
cmd.Flags().StringSlice("teams", nil, "")
cmd.Flags().StringSlice("causes", nil, "")

err := runUpdate(cmd, []string{"INC-42"})
if err == nil {
Expand All @@ -380,6 +405,48 @@ func TestRunUpdateNoFlags(t *testing.T) {
}
}

func TestRunUpdateCanClearAssociations(t *testing.T) {
var attributes map[string]interface{}
setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
var requestBody map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
attributes = requestBody["data"].(map[string]interface{})["attributes"].(map[string]interface{})
w.Header().Set("Content-Type", "application/vnd.api+json")
_, _ = w.Write([]byte(getResponse()))
})
viper.Set("format", "table")

cmd := newTestCmd()
cmd.Flags().String("title", "", "")
cmd.Flags().String("summary", "", "")
cmd.Flags().String("severity", "", "")
cmd.Flags().String("status", "", "")
cmd.Flags().StringSlice("services", nil, "")
cmd.Flags().StringSlice("types", nil, "")
cmd.Flags().StringSlice("functionalities", nil, "")
cmd.Flags().StringSlice("environments", nil, "")
cmd.Flags().StringSlice("teams", nil, "")
cmd.Flags().StringSlice("causes", nil, "")
if err := cmd.Flags().Set("services", ""); err != nil {
t.Fatalf("failed to set empty services: %v", err)
}

captureStdout(t, func() {
if err := runUpdate(cmd, []string{"INC-42"}); err != nil {
t.Fatalf("runUpdate returned error: %v", err)
}
})
services, ok := attributes["service_ids"].([]interface{})
if !ok || len(services) != 0 {
t.Errorf("service_ids = %#v, want an explicitly empty array", attributes["service_ids"])
}
if _, ok := attributes["incident_type_ids"]; ok {
t.Error("incident_type_ids should be absent when --types is omitted")
}
}

func TestRunListNoToken(t *testing.T) {
viper.Reset()
defer viper.Reset()
Expand Down
37 changes: 36 additions & 1 deletion internal/cmd/incidents/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ var createCmd = &cobra.Command{
--title="API degradation" \
--summary="Response times elevated above 2s p99" \
--severity=sev0 \
--services=api-gateway,payments \
--types=customer-impacting \
--functionalities=checkout \
--environments=production \
--teams=platform \
--status=started

# Create and output as JSON
Expand All @@ -34,6 +39,12 @@ func init() {
createCmd.Flags().String("summary", "", "Incident summary/description")
createCmd.Flags().String("severity", "", "Severity ID")
createCmd.Flags().String("status", "", "Initial status (started, mitigated, resolved)")
createCmd.Flags().StringSlice("services", nil, "Service slugs/IDs, comma-separated")
createCmd.Flags().StringSlice("types", nil, "Incident type slugs/IDs, comma-separated")
createCmd.Flags().StringSlice("functionalities", nil, "Functionality slugs/IDs, comma-separated")
createCmd.Flags().StringSlice("environments", nil, "Environment slugs/IDs, comma-separated")
createCmd.Flags().StringSlice("teams", nil, "Team slugs/IDs, comma-separated")
createCmd.Flags().StringSlice("causes", nil, "Cause slugs/IDs, comma-separated")

// Mark title as required
_ = createCmd.MarkFlagRequired("title")
Expand All @@ -54,9 +65,15 @@ func runCreate(cmd *cobra.Command, args []string) error {
summary, _ := cmd.Flags().GetString("summary")
severity, _ := cmd.Flags().GetString("severity")
status, _ := cmd.Flags().GetString("status")
services, _ := cmd.Flags().GetStringSlice("services")
incidentTypes, _ := cmd.Flags().GetStringSlice("types")
functionalities, _ := cmd.Flags().GetStringSlice("functionalities")
environments, _ := cmd.Flags().GetStringSlice("environments")
teams, _ := cmd.Flags().GetStringSlice("teams")
causes, _ := cmd.Flags().GetStringSlice("causes")

// Build opts map - only add keys where the flag was provided
opts := make(map[string]string)
opts := make(map[string]interface{})
if summary != "" {
opts["summary"] = summary
}
Expand All @@ -66,6 +83,24 @@ func runCreate(cmd *cobra.Command, args []string) error {
if status != "" {
opts["status"] = status
}
if len(services) > 0 {
opts["service_ids"] = services
}
if len(incidentTypes) > 0 {
opts["incident_type_ids"] = incidentTypes
}
if len(functionalities) > 0 {
opts["functionality_ids"] = functionalities
}
if len(environments) > 0 {
opts["environment_ids"] = environments
}
if len(teams) > 0 {
opts["group_ids"] = teams
}
if len(causes) > 0 {
opts["cause_ids"] = causes
}

// Call API
incident, err := apiClient.CreateIncident(cmd.Context(), title, opts)
Expand Down
42 changes: 40 additions & 2 deletions internal/cmd/incidents/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ var updateCmd = &cobra.Command{
--summary="Root cause identified: connection pool exhaustion"

# Update severity
rootly incidents update INC-123 --severity=sev1`,
rootly incidents update INC-123 --severity=sev1

# Update attached services and incident types
rootly incidents update INC-123 \
--services=api-gateway,payments \
--types=customer-impacting \
--functionalities=checkout \
--environments=production \
--teams=platform`,
Args: cobra.ExactArgs(1),
RunE: runUpdate,
}
Expand All @@ -34,6 +42,12 @@ func init() {
updateCmd.Flags().String("summary", "", "Updated summary")
updateCmd.Flags().String("severity", "", "Updated severity ID")
updateCmd.Flags().String("status", "", "Updated status (started, mitigated, resolved, closed, cancelled)")
updateCmd.Flags().StringSlice("services", nil, "Updated service slugs/IDs, comma-separated")
updateCmd.Flags().StringSlice("types", nil, "Updated incident type slugs/IDs, comma-separated")
updateCmd.Flags().StringSlice("functionalities", nil, "Updated functionality slugs/IDs, comma-separated")
updateCmd.Flags().StringSlice("environments", nil, "Updated environment slugs/IDs, comma-separated")
updateCmd.Flags().StringSlice("teams", nil, "Updated team slugs/IDs, comma-separated")
updateCmd.Flags().StringSlice("causes", nil, "Updated cause slugs/IDs, comma-separated")

// Register with parent command
IncidentsCmd.AddCommand(updateCmd)
Expand All @@ -50,7 +64,7 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}

// Build opts map using cmd.Flags().Changed() - ONLY include fields the user explicitly set
opts := make(map[string]string)
opts := make(map[string]interface{})
if cmd.Flags().Changed("title") {
title, _ := cmd.Flags().GetString("title")
opts["title"] = title
Expand All @@ -67,6 +81,30 @@ func runUpdate(cmd *cobra.Command, args []string) error {
status, _ := cmd.Flags().GetString("status")
opts["status"] = status
}
if cmd.Flags().Changed("services") {
services, _ := cmd.Flags().GetStringSlice("services")
opts["service_ids"] = services
}
if cmd.Flags().Changed("types") {
incidentTypes, _ := cmd.Flags().GetStringSlice("types")
opts["incident_type_ids"] = incidentTypes
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if cmd.Flags().Changed("functionalities") {
functionalities, _ := cmd.Flags().GetStringSlice("functionalities")
opts["functionality_ids"] = functionalities
}
if cmd.Flags().Changed("environments") {
environments, _ := cmd.Flags().GetStringSlice("environments")
opts["environment_ids"] = environments
}
if cmd.Flags().Changed("teams") {
teams, _ := cmd.Flags().GetStringSlice("teams")
opts["group_ids"] = teams
}
if cmd.Flags().Changed("causes") {
causes, _ := cmd.Flags().GetStringSlice("causes")
opts["cause_ids"] = causes
}

// If opts is empty (no flags changed), return error
if len(opts) == 0 {
Expand Down