diff --git a/cmd/app_keys.go b/cmd/app_keys.go new file mode 100644 index 00000000..10baefb0 --- /dev/null +++ b/cmd/app_keys.go @@ -0,0 +1,221 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package cmd + +import ( + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/DataDog/pup/pkg/formatter" + "github.com/spf13/cobra" +) + +var appKeysCmd = &cobra.Command{ + Use: "app-keys", + Short: "Manage app key registrations", + Long: `Manage Datadog app key registrations for Action Connections. + +App key registrations enable application keys to be used with Action Connections +and Workflow Automation features. This is separate from standard application key +management (see 'pup api-keys' for that). + +CAPABILITIES: + • List registered app keys + • Get app key registration details + • Register an application key for Action Connections + • Unregister an application key from Action Connections + +EXAMPLES: + # List all registered app keys + pup app-keys list + + # Get app key registration details + pup app-keys get + + # Register an application key + pup app-keys register + + # Unregister an application key + pup app-keys unregister + +AUTHENTICATION: + Requires OAuth2 (via 'pup auth login') or valid API + Application keys.`, +} + +var appKeysListCmd = &cobra.Command{ + Use: "list", + Short: "List registered app keys", + Long: `List all app keys registered for Action Connections. + +Returns a paginated list of app key registrations with their IDs and types.`, + RunE: runAppKeysList, +} + +var appKeysGetCmd = &cobra.Command{ + Use: "get [app-key-id]", + Short: "Get app key registration details", + Long: `Get details for a specific app key registration by its ID. + +The app-key-id is the UUID of the registered application key.`, + Args: cobra.ExactArgs(1), + RunE: runAppKeysGet, +} + +var appKeysRegisterCmd = &cobra.Command{ + Use: "register [app-key-id]", + Short: "Register an application key", + Long: `Register an existing application key for use with Action Connections. + +This enables the application key to be used in workflow automation and +Action Connection features. The app-key-id must be the ID of an existing +application key (see 'pup api-keys list' to view application keys). + +EXAMPLES: + # Register an application key + pup app-keys register abc-123-def-456 + + # Register with JSON output + pup app-keys register abc-123-def-456 -o json`, + Args: cobra.ExactArgs(1), + RunE: runAppKeysRegister, +} + +var appKeysUnregisterCmd = &cobra.Command{ + Use: "unregister [app-key-id]", + Short: "Unregister an application key", + Long: `Unregister an application key from Action Connections (DESTRUCTIVE). + +WARNING: This will remove the app key registration, preventing it from being +used with Action Connections and workflow automation features. The underlying +application key itself will NOT be deleted. + +Before unregistering, ensure: + • No active Action Connections are using this key + • No workflows depend on this registration + +Use --auto-approve to skip the confirmation prompt (use with caution).`, + Args: cobra.ExactArgs(1), + RunE: runAppKeysUnregister, +} + +var ( + appKeysPageSize int64 + appKeysPageNumber int64 +) + +func init() { + appKeysListCmd.Flags().Int64Var(&appKeysPageSize, "page-size", 10, "Number of results per page") + appKeysListCmd.Flags().Int64Var(&appKeysPageNumber, "page-number", 0, "Page number to retrieve (0-indexed)") + + appKeysCmd.AddCommand(appKeysListCmd, appKeysGetCmd, appKeysRegisterCmd, appKeysUnregisterCmd) +} + +func runAppKeysList(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewActionConnectionApi(client.V2()) + opts := datadogV2.ListAppKeyRegistrationsOptionalParameters{} + + if appKeysPageSize > 0 { + opts.WithPageSize(appKeysPageSize) + } + if appKeysPageNumber > 0 { + opts.WithPageNumber(appKeysPageNumber) + } + + resp, r, err := api.ListAppKeyRegistrations(client.Context(), opts) + if err != nil { + return formatAPIError("list app key registrations", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runAppKeysGet(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + appKeyID := args[0] + api := datadogV2.NewActionConnectionApi(client.V2()) + + resp, r, err := api.GetAppKeyRegistration(client.Context(), appKeyID) + if err != nil { + return formatAPIError("get app key registration", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runAppKeysRegister(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + appKeyID := args[0] + api := datadogV2.NewActionConnectionApi(client.V2()) + + resp, r, err := api.RegisterAppKey(client.Context(), appKeyID) + if err != nil { + return formatAPIError("register app key", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runAppKeysUnregister(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + appKeyID := args[0] + if !cfg.AutoApprove { + printOutput("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + printOutput("⚠️ DESTRUCTIVE OPERATION WARNING ⚠️\n") + printOutput("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + printOutput("\nYou are about to UNREGISTER app key: %s\n", appKeyID) + printOutput("\nThis action:\n") + printOutput(" • Will remove the app key registration\n") + printOutput(" • May affect Action Connections using this key\n") + printOutput(" • Cannot be undone (must re-register if needed)\n") + printOutput(" • Does NOT delete the underlying application key\n") + printOutput("\nType 'yes' to confirm unregistration: ") + + response, err := readConfirmation() + if err != nil || response != "yes" { + printOutput("\n✓ Operation cancelled\n") + return nil + } + } + + api := datadogV2.NewActionConnectionApi(client.V2()) + r, err := api.UnregisterAppKey(client.Context(), appKeyID) + if err != nil { + return formatAPIError("unregister app key", err, r) + } + + printOutput("Successfully unregistered app key %s\n", appKeyID) + return nil +} diff --git a/cmd/cases.go b/cmd/cases.go new file mode 100644 index 00000000..b6153f0d --- /dev/null +++ b/cmd/cases.go @@ -0,0 +1,667 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package cmd + +import ( + "fmt" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/DataDog/pup/pkg/formatter" + "github.com/spf13/cobra" +) + +var casesCmd = &cobra.Command{ + Use: "cases", + Short: "Manage case management cases and projects", + Long: `Manage Datadog Case Management for tracking and resolving issues. + +Case Management provides structured workflows for handling customer issues, +bugs, and internal requests. Cases can be organized into projects with +custom attributes, priorities, and assignments. + +CAPABILITIES: + • Create and manage cases with custom attributes + • Search and filter cases + • Assign cases to users + • Archive/unarchive cases + • Manage projects + • Add comments and track timelines + +CASE PRIORITIES: + • NOT_DEFINED: No priority set + • P1: Critical priority + • P2: High priority + • P3: Medium priority + • P4: Low priority + • P5: Lowest priority + +EXAMPLES: + # Search cases + pup cases search --query="bug" + + # Get case details + pup cases get case-123 + + # Create a new case + pup cases create --title="Bug report" --type-id="type-uuid" --priority=P2 + + # List projects + pup cases projects list + +AUTHENTICATION: + Requires either OAuth2 authentication (pup auth login) or API keys.`, +} + +var casesSearchCmd = &cobra.Command{ + Use: "search", + Short: "Search cases", + Long: `Search cases with optional filtering. + +FLAGS: + --query Search query string + --page-size Results per page (default: 10) + --page-number Page number (default: 0) + +EXAMPLES: + # Search all cases + pup cases search + + # Search with query + pup cases search --query="bug" + + # Search with pagination + pup cases search --page-size=20 --page-number=1`, + RunE: runCasesSearch, +} + +var casesGetCmd = &cobra.Command{ + Use: "get [case-id]", + Short: "Get case details", + Long: `Get detailed information about a specific case. + +ARGUMENTS: + case-id The case ID + +EXAMPLES: + # Get case details + pup cases get case-123`, + Args: cobra.ExactArgs(1), + RunE: runCasesGet, +} + +var casesCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new case", + Long: `Create a new case with title and type. + +REQUIRED FLAGS: + --title Case title + --type-id Case type UUID + +OPTIONAL FLAGS: + --description Case description + --priority Priority: NOT_DEFINED, P1, P2, P3, P4, P5 (default: NOT_DEFINED) + +EXAMPLES: + # Create basic case + pup cases create --title="Bug report" --type-id="abc-123" + + # Create with priority and description + pup cases create --title="Critical bug" --type-id="abc-123" --priority=P1 --description="Production issue"`, + RunE: runCasesCreate, +} + +var casesArchiveCmd = &cobra.Command{ + Use: "archive [case-id]", + Short: "Archive a case", + Long: `Archive a case to mark it as completed. + +ARGUMENTS: + case-id The case ID + +EXAMPLES: + # Archive case + pup cases archive case-123`, + Args: cobra.ExactArgs(1), + RunE: runCasesArchive, +} + +var casesUnarchiveCmd = &cobra.Command{ + Use: "unarchive [case-id]", + Short: "Unarchive a case", + Long: `Unarchive a case to reopen it. + +ARGUMENTS: + case-id The case ID + +EXAMPLES: + # Unarchive case + pup cases unarchive case-123`, + Args: cobra.ExactArgs(1), + RunE: runCasesUnarchive, +} + +var casesAssignCmd = &cobra.Command{ + Use: "assign [case-id]", + Short: "Assign a case to a user", + Long: `Assign a case to a specific user. + +REQUIRED FLAGS: + --user-id User UUID to assign the case to + +ARGUMENTS: + case-id The case ID + +EXAMPLES: + # Assign case + pup cases assign case-123 --user-id="user-uuid"`, + Args: cobra.ExactArgs(1), + RunE: runCasesAssign, +} + +var casesUpdateTitleCmd = &cobra.Command{ + Use: "update-title [case-id]", + Short: "Update case title", + Long: `Update the title of a case. + +REQUIRED FLAGS: + --title New title + +ARGUMENTS: + case-id The case ID + +EXAMPLES: + # Update title + pup cases update-title case-123 --title="New title"`, + Args: cobra.ExactArgs(1), + RunE: runCasesUpdateTitle, +} + +var casesUpdatePriorityCmd = &cobra.Command{ + Use: "update-priority [case-id]", + Short: "Update case priority", + Long: `Update the priority of a case. + +REQUIRED FLAGS: + --priority New priority: NOT_DEFINED, P1, P2, P3, P4, P5 + +ARGUMENTS: + case-id The case ID + +EXAMPLES: + # Update priority + pup cases update-priority case-123 --priority=P1`, + Args: cobra.ExactArgs(1), + RunE: runCasesUpdatePriority, +} + +// Projects subcommand +var casesProjectsCmd = &cobra.Command{ + Use: "projects", + Short: "Manage case projects", + Long: `Create, list, get, and delete case management projects. + +Projects organize cases into logical groups with shared settings.`, +} + +var casesProjectsListCmd = &cobra.Command{ + Use: "list", + Short: "List all projects", + Long: `List all case management projects. + +EXAMPLES: + # List projects + pup cases projects list`, + RunE: runCasesProjectsList, +} + +var casesProjectsGetCmd = &cobra.Command{ + Use: "get [project-id]", + Short: "Get project details", + Long: `Get detailed information about a project. + +ARGUMENTS: + project-id The project ID + +EXAMPLES: + # Get project details + pup cases projects get project-123`, + Args: cobra.ExactArgs(1), + RunE: runCasesProjectsGet, +} + +var casesProjectsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new project", + Long: `Create a new case management project. + +REQUIRED FLAGS: + --name Project name + --key Project key (short identifier) + +EXAMPLES: + # Create project + pup cases projects create --name="Customer Issues" --key="CUST"`, + RunE: runCasesProjectsCreate, +} + +var casesProjectsDeleteCmd = &cobra.Command{ + Use: "delete [project-id]", + Short: "Delete a project", + Long: `Delete a case management project. + +ARGUMENTS: + project-id The project ID + +FLAGS: + --yes, -y Skip confirmation prompt + +EXAMPLES: + # Delete with confirmation + pup cases projects delete project-123 + + # Delete without confirmation + pup cases projects delete project-123 --yes`, + Args: cobra.ExactArgs(1), + RunE: runCasesProjectsDelete, +} + +var ( + // Case flags + caseTitle string + caseTypeID string + caseDescription string + casePriority string + caseUserID string + caseQuery string + casePageSize int64 + casePageNumber int64 + + // Project flags + projectName string + projectKey string +) + +func init() { + // Search flags + casesSearchCmd.Flags().StringVar(&caseQuery, "query", "", "Search query") + casesSearchCmd.Flags().Int64Var(&casePageSize, "page-size", 10, "Results per page") + casesSearchCmd.Flags().Int64Var(&casePageNumber, "page-number", 0, "Page number") + + // Create flags + casesCreateCmd.Flags().StringVar(&caseTitle, "title", "", "Case title (required)") + casesCreateCmd.Flags().StringVar(&caseTypeID, "type-id", "", "Case type UUID (required)") + casesCreateCmd.Flags().StringVar(&caseDescription, "description", "", "Case description") + casesCreateCmd.Flags().StringVar(&casePriority, "priority", "NOT_DEFINED", "Priority level") + _ = casesCreateCmd.MarkFlagRequired("title") + _ = casesCreateCmd.MarkFlagRequired("type-id") + + // Assign flags + casesAssignCmd.Flags().StringVar(&caseUserID, "user-id", "", "User UUID (required)") + _ = casesAssignCmd.MarkFlagRequired("user-id") + + // Update title flags + casesUpdateTitleCmd.Flags().StringVar(&caseTitle, "title", "", "New title (required)") + _ = casesUpdateTitleCmd.MarkFlagRequired("title") + + // Update priority flags + casesUpdatePriorityCmd.Flags().StringVar(&casePriority, "priority", "", "New priority (required)") + _ = casesUpdatePriorityCmd.MarkFlagRequired("priority") + + // Project create flags + casesProjectsCreateCmd.Flags().StringVar(&projectName, "name", "", "Project name (required)") + casesProjectsCreateCmd.Flags().StringVar(&projectKey, "key", "", "Project key (required)") + _ = casesProjectsCreateCmd.MarkFlagRequired("name") + _ = casesProjectsCreateCmd.MarkFlagRequired("key") + + // Build command hierarchy + casesProjectsCmd.AddCommand( + casesProjectsListCmd, + casesProjectsGetCmd, + casesProjectsCreateCmd, + casesProjectsDeleteCmd, + ) + + casesCmd.AddCommand( + casesSearchCmd, + casesGetCmd, + casesCreateCmd, + casesArchiveCmd, + casesUnarchiveCmd, + casesAssignCmd, + casesUpdateTitleCmd, + casesUpdatePriorityCmd, + casesProjectsCmd, + ) +} + +func runCasesSearch(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewCaseManagementApi(client.V2()) + opts := datadogV2.SearchCasesOptionalParameters{} + + if caseQuery != "" { + opts.WithFilter(caseQuery) + } + if casePageSize > 0 { + opts.WithPageSize(casePageSize) + } + if casePageNumber > 0 { + opts.WithPageNumber(casePageNumber) + } + + resp, r, err := api.SearchCases(client.Context(), opts) + if err != nil { + return formatAPIError("search cases", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesGet(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + caseID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + resp, r, err := api.GetCase(client.Context(), caseID) + if err != nil { + return formatAPIError("get case", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesCreate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + // Build case create request + attributes := datadogV2.NewCaseCreateAttributes(caseTitle, caseTypeID) + + if caseDescription != "" { + attributes.SetDescription(caseDescription) + } + + if casePriority != "" { + priority, err := datadogV2.NewCasePriorityFromValue(casePriority) + if err != nil { + return fmt.Errorf("invalid priority: %w", err) + } + attributes.SetPriority(*priority) + } + + caseData := datadogV2.NewCaseCreate(*attributes, datadogV2.CASERESOURCETYPE_CASE) + body := datadogV2.NewCaseCreateRequest(*caseData) + + api := datadogV2.NewCaseManagementApi(client.V2()) + resp, r, err := api.CreateCase(client.Context(), *body) + if err != nil { + return formatAPIError("create case", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesArchive(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + caseID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + emptyData := datadogV2.NewCaseEmpty(datadogV2.CASERESOURCETYPE_CASE) + body := *datadogV2.NewCaseEmptyRequest(*emptyData) + resp, r, err := api.ArchiveCase(client.Context(), caseID, body) + if err != nil { + return formatAPIError("archive case", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesUnarchive(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + caseID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + emptyData := datadogV2.NewCaseEmpty(datadogV2.CASERESOURCETYPE_CASE) + body := *datadogV2.NewCaseEmptyRequest(*emptyData) + resp, r, err := api.UnarchiveCase(client.Context(), caseID, body) + if err != nil { + return formatAPIError("unarchive case", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesAssign(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + caseID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + // Build assign request (simplified - just assignee ID) + attributes := datadogV2.NewCaseAssignAttributes(caseUserID) + data := datadogV2.NewCaseAssign(*attributes, datadogV2.CASERESOURCETYPE_CASE) + body := datadogV2.NewCaseAssignRequest(*data) + + resp, r, err := api.AssignCase(client.Context(), caseID, *body) + if err != nil { + return formatAPIError("assign case", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesUpdateTitle(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + caseID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + // Build update title request + attributes := datadogV2.NewCaseUpdateTitleAttributes(caseTitle) + data := datadogV2.NewCaseUpdateTitle(*attributes, datadogV2.CASERESOURCETYPE_CASE) + body := datadogV2.NewCaseUpdateTitleRequest(*data) + + resp, r, err := api.UpdateCaseTitle(client.Context(), caseID, *body) + if err != nil { + return formatAPIError("update case title", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesUpdatePriority(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + caseID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + priority, err := datadogV2.NewCasePriorityFromValue(casePriority) + if err != nil { + return fmt.Errorf("invalid priority: %w", err) + } + + // Build update priority request + attributes := datadogV2.NewCaseUpdatePriorityAttributes(*priority) + data := datadogV2.NewCaseUpdatePriority(*attributes, datadogV2.CASERESOURCETYPE_CASE) + body := datadogV2.NewCaseUpdatePriorityRequest(*data) + + resp, r, err := api.UpdatePriority(client.Context(), caseID, *body) + if err != nil { + return formatAPIError("update case priority", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +// Project implementations +func runCasesProjectsList(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewCaseManagementApi(client.V2()) + resp, r, err := api.GetProjects(client.Context()) + if err != nil { + return formatAPIError("list projects", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesProjectsGet(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + projectID := args[0] + api := datadogV2.NewCaseManagementApi(client.V2()) + + resp, r, err := api.GetProject(client.Context(), projectID) + if err != nil { + return formatAPIError("get project", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesProjectsCreate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + // Build project create request + attributes := datadogV2.NewProjectCreateAttributes(projectKey, projectName) + data := datadogV2.NewProjectCreate(*attributes, datadogV2.PROJECTRESOURCETYPE_PROJECT) + body := datadogV2.NewProjectCreateRequest(*data) + + api := datadogV2.NewCaseManagementApi(client.V2()) + resp, r, err := api.CreateProject(client.Context(), *body) + if err != nil { + return formatAPIError("create project", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runCasesProjectsDelete(cmd *cobra.Command, args []string) error { + projectID := args[0] + + // Confirmation prompt unless --yes flag is set + if !cfg.AutoApprove { + printOutput("WARNING: This will permanently delete project '%s'.\n", projectID) + printOutput("Are you sure you want to continue? [y/N]: ") + + response, err := readConfirmation() + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) + } + + if response != "y" && response != "Y" && response != "yes" { + printOutput("Operation cancelled.\n") + return nil + } + } + + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewCaseManagementApi(client.V2()) + r, err := api.DeleteProject(client.Context(), projectID) + if err != nil { + return formatAPIError("delete project", err, r) + } + + printOutput("Project '%s' deleted successfully.\n", projectID) + return nil +} diff --git a/cmd/cost.go b/cmd/cost.go new file mode 100644 index 00000000..13290a5e --- /dev/null +++ b/cmd/cost.go @@ -0,0 +1,263 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package cmd + +import ( + "fmt" + "time" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/DataDog/pup/pkg/formatter" + "github.com/spf13/cobra" +) + +var costCmd = &cobra.Command{ + Use: "cost", + Short: "Manage cost and billing data", + Long: `Query cost management and billing information. + +Access projected costs, cost attribution by tags, and organizational cost breakdowns. +Cost data is typically available with 12-24 hour delay. + +CAPABILITIES: + • View projected end-of-month costs + • Get cost attribution by tags and teams + • Query historical and estimated costs by organization + +EXAMPLES: + # Get projected costs for current month + pup cost projected + + # Get cost attribution by team tag + pup cost attribution --start-month=2024-01 --fields=team + + # Get actual costs for a specific month + pup cost by-org --start-month=2024-01 + +AUTHENTICATION: + Requires OAuth2 (via 'pup auth login') or valid API + Application keys. + Cost management features require billing:read permissions.`, +} + +var costProjectedCmd = &cobra.Command{ + Use: "projected", + Short: "Get projected end-of-month costs", + Long: `Get projected costs for the current month based on usage trends. + +Provides cost projections by product family to help estimate end-of-month bills.`, + RunE: runCostProjected, +} + +var costAttributionCmd = &cobra.Command{ + Use: "attribution", + Short: "Get cost attribution by tags", + Long: `Get monthly cost attribution broken down by tag keys. + +Shows how costs are distributed across different tag values (e.g., teams, services, environments). + +REQUIRED FLAGS: + --start-month Start month in YYYY-MM format + --fields Comma-separated tag keys for breakdown (e.g., "team,env,service") + +OPTIONAL FLAGS: + --end-month End month (defaults to start-month) + +EXAMPLES: + # Get cost by team for January 2024 + pup cost attribution --start-month=2024-01 --fields=team + + # Get cost by multiple dimensions + pup cost attribution --start-month=2024-01 --fields=team,env,service + + # Get cost range + pup cost attribution --start-month=2024-01 --end-month=2024-03 --fields=team`, + RunE: runCostAttribution, +} + +var costByOrgCmd = &cobra.Command{ + Use: "by-org", + Short: "Get costs by organization", + Long: `Get cost breakdown by organization for a specific time period. + +Provides actual, estimated, or historical cost data by organization. + +REQUIRED FLAGS: + --start-month Start month in YYYY-MM format + +OPTIONAL FLAGS: + --end-month End month (defaults to start-month) + --view View type: actual, estimated, historical (default: actual) + +EXAMPLES: + # Get actual costs for January 2024 + pup cost by-org --start-month=2024-01 + + # Get estimated costs + pup cost by-org --start-month=2024-01 --view=estimated + + # Get historical costs + pup cost by-org --start-month=2024-01 --view=historical`, + RunE: runCostByOrg, +} + +var ( + costStartMonth string + costEndMonth string + costFields string + costView string +) + +func init() { + // Attribution flags + costAttributionCmd.Flags().StringVar(&costStartMonth, "start-month", "", "Start month (YYYY-MM) (required)") + costAttributionCmd.Flags().StringVar(&costEndMonth, "end-month", "", "End month (YYYY-MM)") + costAttributionCmd.Flags().StringVar(&costFields, "fields", "", "Tag keys for breakdown (required)") + _ = costAttributionCmd.MarkFlagRequired("start-month") + _ = costAttributionCmd.MarkFlagRequired("fields") + + // By-org flags + costByOrgCmd.Flags().StringVar(&costStartMonth, "start-month", "", "Start month (YYYY-MM) (required)") + costByOrgCmd.Flags().StringVar(&costEndMonth, "end-month", "", "End month (YYYY-MM)") + costByOrgCmd.Flags().StringVar(&costView, "view", "actual", "View type: actual, estimated, historical") + _ = costByOrgCmd.MarkFlagRequired("start-month") + + // Command hierarchy + costCmd.AddCommand(costProjectedCmd, costAttributionCmd, costByOrgCmd) +} + +func runCostProjected(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewUsageMeteringApi(client.V2()) + opts := datadogV2.GetProjectedCostOptionalParameters{} + + resp, r, err := api.GetProjectedCost(client.Context(), opts) + if err != nil { + return formatAPIError("get projected cost", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + + printOutput("%s\n", output) + return nil +} + +func runCostAttribution(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + // Parse start month + startTime, err := time.Parse("2006-01", costStartMonth) + if err != nil { + return fmt.Errorf("invalid start month format (use YYYY-MM): %w", err) + } + + // Parse end month if provided, otherwise use start month + endTime := startTime + if costEndMonth != "" { + endTime, err = time.Parse("2006-01", costEndMonth) + if err != nil { + return fmt.Errorf("invalid end month format (use YYYY-MM): %w", err) + } + } + + api := datadogV2.NewUsageMeteringApi(client.V2()) + opts := datadogV2.GetMonthlyCostAttributionOptionalParameters{} + if costEndMonth != "" { + opts.WithEndMonth(endTime) + } + + resp, r, err := api.GetMonthlyCostAttribution(client.Context(), startTime, costFields, opts) + if err != nil { + return formatAPIError("get cost attribution", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + + printOutput("%s\n", output) + return nil +} + +func runCostByOrg(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + // Parse start month + startTime, err := time.Parse("2006-01", costStartMonth) + if err != nil { + return fmt.Errorf("invalid start month format (use YYYY-MM): %w", err) + } + + api := datadogV2.NewUsageMeteringApi(client.V2()) + + // Call appropriate API based on view type + var resp interface{} + var r interface{} + + switch costView { + case "actual": + opts := datadogV2.GetCostByOrgOptionalParameters{} + if costEndMonth != "" { + endTime, err := time.Parse("2006-01", costEndMonth) + if err != nil { + return fmt.Errorf("invalid end month format (use YYYY-MM): %w", err) + } + opts.WithEndMonth(endTime) + } + resp, r, err = api.GetCostByOrg(client.Context(), startTime, opts) + + case "estimated": + opts := datadogV2.GetEstimatedCostByOrgOptionalParameters{} + opts.WithStartMonth(startTime) + if costEndMonth != "" { + endTime, err := time.Parse("2006-01", costEndMonth) + if err != nil { + return fmt.Errorf("invalid end month format (use YYYY-MM): %w", err) + } + opts.WithEndMonth(endTime) + } + resp, r, err = api.GetEstimatedCostByOrg(client.Context(), opts) + + case "historical": + opts := datadogV2.GetHistoricalCostByOrgOptionalParameters{} + if costEndMonth != "" { + endTime, err := time.Parse("2006-01", costEndMonth) + if err != nil { + return fmt.Errorf("invalid end month format (use YYYY-MM): %w", err) + } + opts.WithEndMonth(endTime) + } + resp, r, err = api.GetHistoricalCostByOrg(client.Context(), startTime, opts) + + default: + return fmt.Errorf("invalid view type '%s': must be actual, estimated, or historical", costView) + } + + if err != nil { + return formatAPIError(fmt.Sprintf("get %s cost by org", costView), err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + + printOutput("%s\n", output) + return nil +} diff --git a/cmd/incidents.go b/cmd/incidents.go index e66303fb..83e90ddc 100644 --- a/cmd/incidents.go +++ b/cmd/incidents.go @@ -211,13 +211,76 @@ USE CASES: • Review incident tasks and completion • Export incident data for postmortems • Monitor customer impact duration`, - Args: cobra.ExactArgs(1), - RunE: runIncidentsGet, + Args: cobra.ExactArgs(1), + RunE: runIncidentsGet, +} + +// Attachments subcommand +var incidentsAttachmentsCmd = &cobra.Command{ + Use: "attachments", + Short: "Manage incident attachments", + Long: `List and delete incident attachments. + +Attachments can include links to runbooks, postmortems, documentation, +and other resources related to the incident. + +ATTACHMENT TYPES: + • link: External link to documentation or resources + • postmortem: Link to incident postmortem + • documentation: Link to related documentation`, +} + +var incidentsAttachmentsListCmd = &cobra.Command{ + Use: "list [incident-id]", + Short: "List incident attachments", + Long: `List all attachments for an incident. + +ARGUMENTS: + incident-id The incident ID (format: xxx-xxx-xxx) + +EXAMPLES: + # List all attachments for an incident + pup incidents attachments list abc-123-def + + # List attachments with table output + pup incidents attachments list abc-123-def --output=table`, + Args: cobra.ExactArgs(1), + RunE: runIncidentsAttachmentsList, +} + +var incidentsAttachmentsDeleteCmd = &cobra.Command{ + Use: "delete [incident-id] [attachment-id]", + Short: "Delete an incident attachment", + Long: `Delete an attachment from an incident. + +ARGUMENTS: + incident-id The incident ID (format: xxx-xxx-xxx) + attachment-id The attachment ID + +FLAGS: + --yes, -y Skip confirmation prompt + +EXAMPLES: + # Delete attachment with confirmation + pup incidents attachments delete abc-123-def attachment-123 + + # Delete without confirmation + pup incidents attachments delete abc-123-def attachment-123 --yes`, + Args: cobra.ExactArgs(2), + RunE: runIncidentsAttachmentsDelete, } func init() { - incidentsCmd.AddCommand(incidentsListCmd) - incidentsCmd.AddCommand(incidentsGetCmd) + incidentsAttachmentsCmd.AddCommand( + incidentsAttachmentsListCmd, + incidentsAttachmentsDeleteCmd, + ) + + incidentsCmd.AddCommand( + incidentsListCmd, + incidentsGetCmd, + incidentsAttachmentsCmd, + ) } func runIncidentsList(cmd *cobra.Command, args []string) error { @@ -227,13 +290,9 @@ func runIncidentsList(cmd *cobra.Command, args []string) error { } api := datadogV2.NewIncidentsApi(client.V2()) - resp, r, err := api.ListIncidents(client.Context()) if err != nil { - if r != nil { - return fmt.Errorf("failed to list incidents: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to list incidents: %w", err) + return formatAPIError("list incidents", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) @@ -256,10 +315,7 @@ func runIncidentsGet(cmd *cobra.Command, args []string) error { resp, r, err := api.GetIncident(client.Context(), incidentID) if err != nil { - if r != nil { - return fmt.Errorf("failed to get incident: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to get incident: %w", err) + return formatAPIError("get incident", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) @@ -270,3 +326,62 @@ func runIncidentsGet(cmd *cobra.Command, args []string) error { printOutput("%s\n", output) return nil } + +// Attachment implementations +func runIncidentsAttachmentsList(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + incidentID := args[0] + api := datadogV2.NewIncidentsApi(client.V2()) + + resp, r, err := api.ListIncidentAttachments(client.Context(), incidentID) + if err != nil { + return formatAPIError("list incident attachments", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + + printOutput("%s\n", output) + return nil +} + +func runIncidentsAttachmentsDelete(cmd *cobra.Command, args []string) error { + incidentID := args[0] + attachmentID := args[1] + + // Confirmation prompt unless --yes flag is set + if !cfg.AutoApprove { + printOutput("WARNING: This will permanently delete attachment '%s' from incident '%s'.\n", attachmentID, incidentID) + printOutput("Are you sure you want to continue? [y/N]: ") + + response, err := readConfirmation() + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) + } + + if response != "y" && response != "Y" && response != "yes" { + printOutput("Operation cancelled.\n") + return nil + } + } + + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewIncidentsApi(client.V2()) + r, err := api.DeleteIncidentAttachment(client.Context(), incidentID, attachmentID) + if err != nil { + return formatAPIError("delete incident attachment", err, r) + } + + printOutput("Attachment '%s' deleted successfully from incident '%s'.\n", attachmentID, incidentID) + return nil +} diff --git a/cmd/monitors.go b/cmd/monitors.go index f1231239..71ca9c1e 100644 --- a/cmd/monitors.go +++ b/cmd/monitors.go @@ -212,10 +212,40 @@ WARNING: RunE: runMonitorsDelete, } +var monitorsSearchCmd = &cobra.Command{ + Use: "search", + Short: "Search monitors", + Long: `Search monitors using a query string. + +Search allows more flexible querying than list filtering, supporting +advanced search syntax for finding specific monitors. + +FLAGS: + --query Search query string + --page Page number (default: 0) + --per-page Results per page (default: 30) + --sort Sort order (e.g., "name,asc", "id,desc") + +EXAMPLES: + # Search for monitors by text + pup monitors search --query="database" + + # Search with pagination + pup monitors search --query="cpu" --page=1 --per-page=50 + + # Search and sort + pup monitors search --query="memory" --sort="name,asc"`, + RunE: runMonitorsSearch, +} + var ( - monitorName string - monitorTags string - monitorLimit int32 + monitorName string + monitorTags string + monitorLimit int32 + searchQuery string + searchPage int64 + searchPerPage int64 + searchSort string ) func init() { @@ -223,9 +253,17 @@ func init() { monitorsListCmd.Flags().StringVar(&monitorTags, "tags", "", "Filter monitors by tags (comma-separated)") monitorsListCmd.Flags().Int32Var(&monitorLimit, "limit", 200, "Maximum number of monitors to return (default: 200, max: 1000)") - monitorsCmd.AddCommand(monitorsListCmd) - monitorsCmd.AddCommand(monitorsGetCmd) - monitorsCmd.AddCommand(monitorsDeleteCmd) + monitorsSearchCmd.Flags().StringVar(&searchQuery, "query", "", "Search query string") + monitorsSearchCmd.Flags().Int64Var(&searchPage, "page", 0, "Page number") + monitorsSearchCmd.Flags().Int64Var(&searchPerPage, "per-page", 30, "Results per page") + monitorsSearchCmd.Flags().StringVar(&searchSort, "sort", "", "Sort order") + + monitorsCmd.AddCommand( + monitorsListCmd, + monitorsGetCmd, + monitorsDeleteCmd, + monitorsSearchCmd, + ) } func runMonitorsList(cmd *cobra.Command, args []string) error { @@ -359,3 +397,39 @@ func runMonitorsDelete(cmd *cobra.Command, args []string) error { printOutput("%s\n", output) return nil } + +func runMonitorsSearch(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + api := datadogV1.NewMonitorsApi(client.V1()) + opts := datadogV1.SearchMonitorsOptionalParameters{} + + if searchQuery != "" { + opts.WithQuery(searchQuery) + } + if searchPage > 0 { + opts.WithPage(searchPage) + } + if searchPerPage > 0 { + opts.WithPerPage(searchPerPage) + } + if searchSort != "" { + opts.WithSort(searchSort) + } + + resp, r, err := api.SearchMonitors(client.Context(), opts) + if err != nil { + return formatAPIError("search monitors", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + + printOutput("%s\n", output) + return nil +} diff --git a/cmd/on_call.go b/cmd/on_call.go index e022adaa..f5f77a80 100644 --- a/cmd/on_call.go +++ b/cmd/on_call.go @@ -15,46 +15,288 @@ import ( var onCallCmd = &cobra.Command{ Use: "on-call", - Short: "Manage on-call teams and schedules", - Long: `Manage on-call teams, schedules, and rotations. + Short: "Manage teams and on-call operations", + Long: `Manage teams, memberships, links, and notification rules. + +Teams in Datadog represent groups of users that collaborate on monitoring, +incident response, and on-call duties. Use this command to manage team +structure, members, and notification settings. CAPABILITIES: - • Manage on-call teams - • View and manage schedules - • Handle schedule overrides - • Track incidents and escalations + • Create, update, and delete teams + • Manage team memberships and roles + • Configure team links (documentation, runbooks) + • Set up notification rules for team alerts EXAMPLES: # List all teams pup on-call teams list - # Get team details - pup on-call teams get team-id + # Create a new team + pup on-call teams create --name="SRE Team" --handle="sre-team" + + # Add a member to a team + pup on-call teams memberships add --user-id= --role=member + + # List team members + pup on-call teams memberships list AUTHENTICATION: - Requires either OAuth2 authentication or API keys.`, + Requires either OAuth2 authentication (pup auth login) or API keys.`, } var onCallTeamsCmd = &cobra.Command{ Use: "teams", - Short: "Manage on-call teams", + Short: "Manage teams", + Long: `Create, update, delete, and query teams. + +Teams are groups of users in your Datadog organization that collaborate +on monitoring, incident response, and on-call rotations.`, } var onCallTeamsListCmd = &cobra.Command{ Use: "list", - Short: "List on-call teams", - RunE: runOnCallTeamsList, + Short: "List all teams", + Long: `List all teams in your organization. + +Supports pagination and filtering options.`, + RunE: runOnCallTeamsList, } var onCallTeamsGetCmd = &cobra.Command{ Use: "get [team-id]", Short: "Get team details", - Args: cobra.ExactArgs(1), - RunE: runOnCallTeamsGet, + Long: `Get detailed information about a specific team. + +The team-id is the unique identifier for the team.`, + Args: cobra.ExactArgs(1), + RunE: runOnCallTeamsGet, +} + +var onCallTeamsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new team", + Long: `Create a new team in your organization. + +REQUIRED FLAGS: + --name Team display name + --handle Team handle (unique identifier, lowercase, no spaces) + +OPTIONAL FLAGS: + --description Team description + --avatar Team avatar URL + --color Team color (hex code, e.g., #00FF00) + --hidden Hide team from UI + +EXAMPLES: + # Create a basic team + pup on-call teams create --name="SRE Team" --handle="sre-team" + + # Create with full details + pup on-call teams create --name="Platform Engineering" --handle="platform-eng" \ + --description="Platform infrastructure team" --color="#FF5733"`, + RunE: runOnCallTeamsCreate, +} + +var onCallTeamsUpdateCmd = &cobra.Command{ + Use: "update [team-id]", + Short: "Update team details", + Long: `Update an existing team's attributes. + +REQUIRED FLAGS: + --name Team display name + --handle Team handle + +OPTIONAL FLAGS: + --description Team description + --avatar Team avatar URL + --hidden Hide team from UI + +EXAMPLES: + # Update team name and handle + pup on-call teams update abc-123 --name="New Team Name" --handle="new-team" + + # Update with description + pup on-call teams update abc-123 --name="SRE" --handle="sre" --description="Site Reliability"`, + Args: cobra.ExactArgs(1), + RunE: runOnCallTeamsUpdate, +} + +var onCallTeamsDeleteCmd = &cobra.Command{ + Use: "delete [team-id]", + Short: "Delete a team", + Long: `Delete a team from your organization. + +WARNING: This is a destructive operation. Team deletion will: + • Remove all team memberships + • Delete all team links and notification rules + • Remove team associations from resources + +Use --yes to skip confirmation prompt. + +EXAMPLES: + # Delete with confirmation + pup on-call teams delete abc-123 + + # Delete without confirmation + pup on-call teams delete abc-123 --yes`, + Args: cobra.ExactArgs(1), + RunE: runOnCallTeamsDelete, +} + +// Team Memberships subcommand +var onCallTeamsMembershipsCmd = &cobra.Command{ + Use: "memberships", + Short: "Manage team memberships", + Long: `Add, update, remove, and list team members. + +Team members can have different roles: + • member: Regular team member with standard permissions + • admin: Team administrator with full permissions`, +} + +var onCallTeamsMembershipsListCmd = &cobra.Command{ + Use: "list [team-id]", + Short: "List team members", + Long: `List all members of a team. + +Supports pagination for teams with many members. + +FLAGS: + --page-size Number of results per page (default: 100) + --page-number Page number to retrieve (default: 0) + --sort Sort order: name, email (default: name) + +EXAMPLES: + # List all members + pup on-call teams memberships list abc-123 + + # List with pagination + pup on-call teams memberships list abc-123 --page-size=50 --page-number=1`, + Args: cobra.ExactArgs(1), + RunE: runOnCallTeamsMembershipsList, +} + +var onCallTeamsMembershipsAddCmd = &cobra.Command{ + Use: "add [team-id]", + Short: "Add a member to team", + Long: `Add a user to a team with a specified role. + +REQUIRED FLAGS: + --user-id User UUID to add to team + --role Member role: member or admin (default: member) + +EXAMPLES: + # Add as regular member + pup on-call teams memberships add abc-123 --user-id=user-uuid-here --role=member + + # Add as admin + pup on-call teams memberships add abc-123 --user-id=user-uuid-here --role=admin`, + Args: cobra.ExactArgs(1), + RunE: runOnCallTeamsMembershipsAdd, +} + +var onCallTeamsMembershipsUpdateCmd = &cobra.Command{ + Use: "update [team-id] [user-id]", + Short: "Update member role", + Long: `Update a team member's role. + +REQUIRED FLAGS: + --role New role: member or admin + +EXAMPLES: + # Promote to admin + pup on-call teams memberships update abc-123 user-uuid --role=admin + + # Demote to member + pup on-call teams memberships update abc-123 user-uuid --role=member`, + Args: cobra.ExactArgs(2), + RunE: runOnCallTeamsMembershipsUpdate, +} + +var onCallTeamsMembershipsRemoveCmd = &cobra.Command{ + Use: "remove [team-id] [user-id]", + Short: "Remove member from team", + Long: `Remove a user from a team. + +Use --yes to skip confirmation prompt. + +EXAMPLES: + # Remove with confirmation + pup on-call teams memberships remove abc-123 user-uuid + + # Remove without confirmation + pup on-call teams memberships remove abc-123 user-uuid --yes`, + Args: cobra.ExactArgs(2), + RunE: runOnCallTeamsMembershipsRemove, } +var ( + // Team flags + teamName string + teamHandle string + teamDescription string + teamAvatar string + teamHidden bool + + // Membership flags + memberUserID string + memberRole string + memberPageSize int64 + memberPageNum int64 + memberSort string +) + func init() { - onCallTeamsCmd.AddCommand(onCallTeamsListCmd, onCallTeamsGetCmd) + // Team create flags + onCallTeamsCreateCmd.Flags().StringVar(&teamName, "name", "", "Team display name (required)") + onCallTeamsCreateCmd.Flags().StringVar(&teamHandle, "handle", "", "Team handle (required)") + onCallTeamsCreateCmd.Flags().StringVar(&teamDescription, "description", "", "Team description") + onCallTeamsCreateCmd.Flags().StringVar(&teamAvatar, "avatar", "", "Team avatar URL") + onCallTeamsCreateCmd.Flags().BoolVar(&teamHidden, "hidden", false, "Hide team from UI") + _ = onCallTeamsCreateCmd.MarkFlagRequired("name") + _ = onCallTeamsCreateCmd.MarkFlagRequired("handle") + + // Team update flags + onCallTeamsUpdateCmd.Flags().StringVar(&teamName, "name", "", "Team display name (required)") + onCallTeamsUpdateCmd.Flags().StringVar(&teamHandle, "handle", "", "Team handle (required)") + onCallTeamsUpdateCmd.Flags().StringVar(&teamDescription, "description", "", "Team description") + onCallTeamsUpdateCmd.Flags().StringVar(&teamAvatar, "avatar", "", "Team avatar URL") + onCallTeamsUpdateCmd.Flags().BoolVar(&teamHidden, "hidden", false, "Hide team from UI") + _ = onCallTeamsUpdateCmd.MarkFlagRequired("name") + _ = onCallTeamsUpdateCmd.MarkFlagRequired("handle") + + // Membership list flags + onCallTeamsMembershipsListCmd.Flags().Int64Var(&memberPageSize, "page-size", 100, "Results per page") + onCallTeamsMembershipsListCmd.Flags().Int64Var(&memberPageNum, "page-number", 0, "Page number") + onCallTeamsMembershipsListCmd.Flags().StringVar(&memberSort, "sort", "name", "Sort order: name, email") + + // Membership add flags + onCallTeamsMembershipsAddCmd.Flags().StringVar(&memberUserID, "user-id", "", "User UUID (required)") + onCallTeamsMembershipsAddCmd.Flags().StringVar(&memberRole, "role", "member", "Role: member or admin") + _ = onCallTeamsMembershipsAddCmd.MarkFlagRequired("user-id") + + // Membership update flags + onCallTeamsMembershipsUpdateCmd.Flags().StringVar(&memberRole, "role", "", "Role: member or admin (required)") + _ = onCallTeamsMembershipsUpdateCmd.MarkFlagRequired("role") + + // Build command hierarchy + onCallTeamsMembershipsCmd.AddCommand( + onCallTeamsMembershipsListCmd, + onCallTeamsMembershipsAddCmd, + onCallTeamsMembershipsUpdateCmd, + onCallTeamsMembershipsRemoveCmd, + ) + + onCallTeamsCmd.AddCommand( + onCallTeamsListCmd, + onCallTeamsGetCmd, + onCallTeamsCreateCmd, + onCallTeamsUpdateCmd, + onCallTeamsDeleteCmd, + onCallTeamsMembershipsCmd, + ) + onCallCmd.AddCommand(onCallTeamsCmd) } @@ -67,17 +309,14 @@ func runOnCallTeamsList(cmd *cobra.Command, args []string) error { api := datadogV2.NewTeamsApi(client.V2()) resp, r, err := api.ListTeams(client.Context()) if err != nil { - if r != nil { - return fmt.Errorf("failed to list teams: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to list teams: %w", err) + return formatAPIError("list teams", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) if err != nil { return err } - fmt.Println(output) + printOutput("%s\n", output) return nil } @@ -91,16 +330,273 @@ func runOnCallTeamsGet(cmd *cobra.Command, args []string) error { api := datadogV2.NewTeamsApi(client.V2()) resp, r, err := api.GetTeam(client.Context(), teamID) if err != nil { - if r != nil { - return fmt.Errorf("failed to get team: %w (status: %d)", err, r.StatusCode) + return formatAPIError("get team", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runOnCallTeamsCreate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + // Build team create request + attributes := datadogV2.NewTeamCreateAttributes(teamHandle, teamName) + + if teamDescription != "" { + attributes.SetDescription(teamDescription) + } + if teamAvatar != "" { + attributes.SetAvatar(teamAvatar) + } + if teamHidden { + attributes.SetHiddenModules([]string{"hidden"}) + } + + teamData := datadogV2.NewTeamCreate(*attributes, datadogV2.TEAMTYPE_TEAM) + body := datadogV2.NewTeamCreateRequest(*teamData) + + api := datadogV2.NewTeamsApi(client.V2()) + resp, r, err := api.CreateTeam(client.Context(), *body) + if err != nil { + return formatAPIError("create team", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runOnCallTeamsUpdate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + teamID := args[0] + + // Build team update request (handle and name are required) + attributes := datadogV2.NewTeamUpdateAttributes(teamHandle, teamName) + + if teamDescription != "" { + attributes.SetDescription(teamDescription) + } + if teamAvatar != "" { + attributes.SetAvatar(teamAvatar) + } + if teamHidden { + attributes.SetHiddenModules([]string{"hidden"}) + } + + teamData := datadogV2.NewTeamUpdate(*attributes, datadogV2.TEAMTYPE_TEAM) + body := datadogV2.NewTeamUpdateRequest(*teamData) + + api := datadogV2.NewTeamsApi(client.V2()) + resp, r, err := api.UpdateTeam(client.Context(), teamID, *body) + if err != nil { + return formatAPIError("update team", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runOnCallTeamsDelete(cmd *cobra.Command, args []string) error { + teamID := args[0] + + // Confirmation prompt unless --yes flag is set + if !cfg.AutoApprove { + printOutput("WARNING: This will permanently delete team '%s' and all associated data.\n", teamID) + printOutput("Are you sure you want to continue? [y/N]: ") + + response, err := readConfirmation() + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) } - return fmt.Errorf("failed to get team: %w", err) + + if response != "y" && response != "Y" && response != "yes" { + printOutput("Operation cancelled.\n") + return nil + } + } + + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewTeamsApi(client.V2()) + r, err := api.DeleteTeam(client.Context(), teamID) + if err != nil { + return formatAPIError("delete team", err, r) + } + + printOutput("Team '%s' deleted successfully.\n", teamID) + return nil +} + +// Team Memberships implementations +func runOnCallTeamsMembershipsList(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + teamID := args[0] + api := datadogV2.NewTeamsApi(client.V2()) + + opts := datadogV2.GetTeamMembershipsOptionalParameters{} + if memberPageSize > 0 { + opts.WithPageSize(memberPageSize) + } + if memberPageNum > 0 { + opts.WithPageNumber(memberPageNum) + } + if memberSort != "" { + sortVal, err := datadogV2.NewGetTeamMembershipsSortFromValue(memberSort) + if err != nil { + return fmt.Errorf("invalid sort value: %w", err) + } + opts.WithSort(*sortVal) + } + + resp, r, err := api.GetTeamMemberships(client.Context(), teamID, opts) + if err != nil { + return formatAPIError("list team memberships", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runOnCallTeamsMembershipsAdd(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + teamID := args[0] + + // Build membership request + userData := datadogV2.NewRelationshipToUserTeamUserData(memberUserID, datadogV2.USERTEAMUSERTYPE_USERS) + user := datadogV2.NewRelationshipToUserTeamUser(*userData) + + teamData := datadogV2.NewRelationshipToUserTeamTeamData(teamID, datadogV2.USERTEAMTEAMTYPE_TEAM) + team := datadogV2.NewRelationshipToUserTeamTeam(*teamData) + + relationships := datadogV2.NewUserTeamRelationships() + relationships.SetUser(*user) + relationships.SetTeam(*team) + + userTeam := datadogV2.NewUserTeamCreate(datadogV2.USERTEAMTYPE_TEAM_MEMBERSHIPS) + userTeam.SetRelationships(*relationships) + + if memberRole != "" { + attributes := datadogV2.NewUserTeamAttributes() + role := datadogV2.UserTeamRole(memberRole) + attributes.SetRole(role) + userTeam.SetAttributes(*attributes) + } + + body := datadogV2.NewUserTeamRequest(*userTeam) + + api := datadogV2.NewTeamsApi(client.V2()) + resp, r, err := api.CreateTeamMembership(client.Context(), teamID, *body) + if err != nil { + return formatAPIError("add team member", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) if err != nil { return err } - fmt.Println(output) + printOutput("%s\n", output) + return nil +} + +func runOnCallTeamsMembershipsUpdate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + teamID := args[0] + userID := args[1] + + // Build membership update request + attributes := datadogV2.NewUserTeamAttributes() + if memberRole != "" { + role := datadogV2.UserTeamRole(memberRole) + attributes.SetRole(role) + } + + userTeam := datadogV2.NewUserTeamUpdate(datadogV2.USERTEAMTYPE_TEAM_MEMBERSHIPS) + userTeam.SetAttributes(*attributes) + body := datadogV2.NewUserTeamUpdateRequest(*userTeam) + + api := datadogV2.NewTeamsApi(client.V2()) + resp, r, err := api.UpdateTeamMembership(client.Context(), teamID, userID, *body) + if err != nil { + return formatAPIError("update team membership", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runOnCallTeamsMembershipsRemove(cmd *cobra.Command, args []string) error { + teamID := args[0] + userID := args[1] + + // Confirmation prompt unless --yes flag is set + if !cfg.AutoApprove { + printOutput("WARNING: This will remove user '%s' from team '%s'.\n", userID, teamID) + printOutput("Are you sure you want to continue? [y/N]: ") + + response, err := readConfirmation() + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) + } + + if response != "y" && response != "Y" && response != "yes" { + printOutput("Operation cancelled.\n") + return nil + } + } + + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewTeamsApi(client.V2()) + r, err := api.DeleteTeamMembership(client.Context(), teamID, userID) + if err != nil { + return formatAPIError("remove team member", err, r) + } + + printOutput("User '%s' removed from team '%s' successfully.\n", userID, teamID) return nil } diff --git a/cmd/product_analytics.go b/cmd/product_analytics.go new file mode 100644 index 00000000..c01c7b88 --- /dev/null +++ b/cmd/product_analytics.go @@ -0,0 +1,156 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/DataDog/pup/pkg/formatter" + "github.com/spf13/cobra" +) + +var productAnalyticsCmd = &cobra.Command{ + Use: "product-analytics", + Short: "Send product analytics events", + Long: `Send server-side product analytics events to Datadog. + +Product Analytics provides insights into user behavior and product usage +through server-side event tracking. + +CAPABILITIES: + • Send individual server-side events with custom properties + +EXAMPLES: + # Send a basic event + pup product-analytics events send \ + --app-id=my-app \ + --event=button_clicked + + # Send event with properties and user context + pup product-analytics events send \ + --app-id=my-app \ + --event=purchase_completed \ + --properties='{"amount":99.99,"currency":"USD"}' \ + --user-id=user-123 + +AUTHENTICATION: + Requires OAuth2 (via 'pup auth login') or valid API + Application keys.`, +} + +var productAnalyticsEventsCmd = &cobra.Command{ + Use: "events", + Short: "Send product analytics events", +} + +var productAnalyticsEventsSendCmd = &cobra.Command{ + Use: "send", + Short: "Send a product analytics event", + Long: `Send a single server-side product analytics event. + +REQUIRED FLAGS: + --app-id Application ID + --event Event name + +OPTIONAL FLAGS: + --properties Event properties as JSON object (default: {}) + --user-id User ID + +EXAMPLES: + # Basic event + pup product-analytics events send \ + --app-id=my-app \ + --event=page_view + + # Event with properties and user + pup product-analytics events send \ + --app-id=my-app \ + --event=purchase_completed \ + --properties='{"product_id":"abc-123","amount":99.99}' \ + --user-id=user-456 + + # Event with JSON output + pup product-analytics events send \ + --app-id=my-app \ + --event=signup \ + --user-id=user-789 \ + --output=json`, + RunE: runProductAnalyticsEventsSend, +} + +var ( + paAppID string + paEventName string + paEventProps string + paUserID string +) + +func init() { + // Send event flags + productAnalyticsEventsSendCmd.Flags().StringVar(&paAppID, "app-id", "", "Application ID (required)") + productAnalyticsEventsSendCmd.Flags().StringVar(&paEventName, "event", "", "Event name (required)") + productAnalyticsEventsSendCmd.Flags().StringVar(&paEventProps, "properties", "{}", "Event properties (JSON object)") + productAnalyticsEventsSendCmd.Flags().StringVar(&paUserID, "user-id", "", "User ID") + + _ = productAnalyticsEventsSendCmd.MarkFlagRequired("app-id") + _ = productAnalyticsEventsSendCmd.MarkFlagRequired("event") + + // Command hierarchy + productAnalyticsEventsCmd.AddCommand(productAnalyticsEventsSendCmd) + productAnalyticsCmd.AddCommand(productAnalyticsEventsCmd) +} + +func runProductAnalyticsEventsSend(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + // Parse event properties JSON + var properties map[string]interface{} + if paEventProps != "" { + if err := json.Unmarshal([]byte(paEventProps), &properties); err != nil { + return fmt.Errorf("invalid event properties JSON: %w", err) + } + } + + // Build application object + app := datadogV2.NewProductAnalyticsServerSideEventItemApplication(paAppID) + + // Build event object with name + event := datadogV2.NewProductAnalyticsServerSideEventItemEvent(paEventName) + + // Add properties as additional properties on the event + if properties != nil && len(properties) > 0 { + event.AdditionalProperties = properties + } + + // Build event item + eventType := datadogV2.PRODUCTANALYTICSSERVERSIDEEVENTITEMTYPE_SERVER + eventItem := datadogV2.NewProductAnalyticsServerSideEventItem(*app, *event, eventType) + + // Add user if provided + if paUserID != "" { + usr := datadogV2.NewProductAnalyticsServerSideEventItemUsr(paUserID) + eventItem.SetUsr(*usr) + } + + api := datadogV2.NewProductAnalyticsApi(client.V2()) + + resp, r, err := api.SubmitProductAnalyticsEvent(client.Context(), *eventItem) + if err != nil { + return formatAPIError("submit product analytics event", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + + printOutput("%s\n", output) + return nil +} diff --git a/cmd/root.go b/cmd/root.go index aea872a8..1893916b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -80,6 +80,7 @@ func init() { rootCmd.AddCommand(onCallCmd) rootCmd.AddCommand(auditLogsCmd) rootCmd.AddCommand(apiKeysCmd) + rootCmd.AddCommand(appKeysCmd) rootCmd.AddCommand(infrastructureCmd) rootCmd.AddCommand(syntheticsCmd) rootCmd.AddCommand(usersCmd) @@ -90,6 +91,7 @@ func init() { rootCmd.AddCommand(errorTrackingCmd) rootCmd.AddCommand(scorecardsCmd) rootCmd.AddCommand(usageCmd) + rootCmd.AddCommand(costCmd) rootCmd.AddCommand(dataGovernanceCmd) rootCmd.AddCommand(obsPipelinesCmd) rootCmd.AddCommand(networkCmd) @@ -97,6 +99,8 @@ func init() { rootCmd.AddCommand(integrationsCmd) rootCmd.AddCommand(miscCmd) rootCmd.AddCommand(investigationsCmd) + rootCmd.AddCommand(productAnalyticsCmd) + rootCmd.AddCommand(casesCmd) } // initConfig reads in config file and ENV variables if set. diff --git a/cmd/rum.go b/cmd/rum.go index 54fb8eb5..3eaabd66 100644 --- a/cmd/rum.go +++ b/cmd/rum.go @@ -548,49 +548,106 @@ func runRumAppsDelete(cmd *cobra.Command, args []string) error { // RUM Metrics Implementation func runRumMetricsList(cmd *cobra.Command, args []string) error { - // NOTE: RUMMetricsApi is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM metrics API is not available in the current API client version") + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewRumMetricsApi(client.V2()) + resp, r, err := api.ListRumMetrics(client.Context()) + if err != nil { + return formatAPIError("list RUM metrics", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil } func runRumMetricsGet(cmd *cobra.Command, args []string) error { - // NOTE: RUMMetricsApi is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM metrics API is not available in the current API client version") + client, err := getClient() + if err != nil { + return err + } + + metricID := args[0] + api := datadogV2.NewRumMetricsApi(client.V2()) + resp, r, err := api.GetRumMetric(client.Context(), metricID) + if err != nil { + return formatAPIError("get RUM metric", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil } func runRumMetricsCreate(cmd *cobra.Command, args []string) error { - // NOTE: RUMMetricsApi is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM metrics API is not available in the current API client version") + return fmt.Errorf("RUM metrics create is not yet implemented - API client types require additional mapping") } func runRumMetricsUpdate(cmd *cobra.Command, args []string) error { - // NOTE: RUMMetricsApi is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM metrics API is not available in the current API client version") + return fmt.Errorf("RUM metrics update is not yet implemented - API client types require additional mapping") } func runRumMetricsDelete(cmd *cobra.Command, args []string) error { - // NOTE: RUMMetricsApi is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM metrics API is not available in the current API client version") + return fmt.Errorf("RUM metrics delete is not yet implemented - API client types require additional mapping") } // RUM Retention Filters Implementation func runRumRetentionFiltersList(cmd *cobra.Command, args []string) error { - // NOTE: RUM Retention Filters API is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM retention filters API is not available in the current API client version") + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewRumRetentionFiltersApi(client.V2()) + resp, r, err := api.ListRetentionFilters(client.Context(), rumAppID) + if err != nil { + return formatAPIError("list RUM retention filters", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil } func runRumRetentionFiltersGet(cmd *cobra.Command, args []string) error { - // NOTE: RUM Retention Filters API is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM retention filters API is not available in the current API client version") + client, err := getClient() + if err != nil { + return err + } + + filterID := args[0] + api := datadogV2.NewRumRetentionFiltersApi(client.V2()) + resp, r, err := api.GetRetentionFilter(client.Context(), rumAppID, filterID) + if err != nil { + return formatAPIError("get RUM retention filter", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil } func runRumRetentionFiltersCreate(cmd *cobra.Command, args []string) error { - // NOTE: RUM Retention Filters API is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM retention filters API is not available in the current API client version") + return fmt.Errorf("RUM retention filters create is not yet implemented - API client types require additional mapping") } func runRumRetentionFiltersUpdate(cmd *cobra.Command, args []string) error { - // NOTE: RUM Retention Filters API is not available in datadog-api-client-go v2.30.0 - return fmt.Errorf("RUM retention filters API is not available in the current API client version") + return fmt.Errorf("RUM retention filters update is not yet implemented - API client types require additional mapping") } func runRumRetentionFiltersDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/security.go b/cmd/security.go index b7e2fd9f..c96bd1b4 100644 --- a/cmd/security.go +++ b/cmd/security.go @@ -70,18 +70,102 @@ var securitySignalsListCmd = &cobra.Command{ var securityFindingsCmd = &cobra.Command{ Use: "findings", Short: "Manage security findings", + Long: `Manage security findings from Datadog's Security Findings API. + +Security findings provide insights into security posture and vulnerabilities +across your infrastructure and applications.`, } var securityFindingsListCmd = &cobra.Command{ Use: "list", Short: "List security findings", - RunE: runSecurityFindingsList, + Long: `List security findings with optional filtering and pagination. + +EXAMPLES: + # List all findings + pup security findings list + + # Filter by status + pup security findings list --status=critical + + # Paginate results + pup security findings list --page-size=50 --page-number=1`, + RunE: runSecurityFindingsList, } +var securityFindingsGetCmd = &cobra.Command{ + Use: "get [finding-id]", + Short: "Get security finding details", + Long: `Get detailed information about a specific security finding. + +EXAMPLES: + # Get finding details + pup security findings get finding-abc-123 + + # Get finding with table output + pup security findings get finding-abc-123 --output=table`, + Args: cobra.ExactArgs(1), + RunE: runSecurityFindingsGet, +} + +var securityFindingsSearchCmd = &cobra.Command{ + Use: "search", + Short: "Search security findings", + Long: `Search security findings using log search syntax. + +QUERY SYNTAX (using log search syntax): + • @severity:(critical OR high) - Filter by severity level + • @status:open - Filter by status + • @attributes.resource_type:s3_bucket - Filter by resource type + • team:platform - Filter by tags (no @ prefix) + • AND, OR, NOT - Boolean operators + +EXAMPLES: + # Search critical or high severity findings + pup security findings search --query="@severity:(critical OR high)" + + # Search open findings with specific resource type and team tag + pup security findings search --query="@status:open @attributes.resource_type:s3_bucket team:platform" + + # Limit results + pup security findings search --query="@severity:critical" --limit=50`, + RunE: runSecurityFindingsSearch, +} + +var ( + // Findings list flags + findingsPageSize int64 + findingsStatus string + findingsEvaluation string + findingsRuleID string + findingsResourceType string + + // Findings search flags + findingsQuery string + findingsLimit int32 + findingsPageCursor string + findingsSort string +) + func init() { + // Findings list flags + securityFindingsListCmd.Flags().Int64Var(&findingsPageSize, "page-size", 100, "Number of findings per page (max: 1000)") + securityFindingsListCmd.Flags().StringVar(&findingsPageCursor, "page-cursor", "", "Page cursor for pagination") + securityFindingsListCmd.Flags().StringVar(&findingsStatus, "status", "", "Filter by status: critical, high, medium, low, info") + securityFindingsListCmd.Flags().StringVar(&findingsEvaluation, "evaluation", "", "Filter by evaluation: pass, fail") + securityFindingsListCmd.Flags().StringVar(&findingsRuleID, "rule-id", "", "Filter by rule ID") + securityFindingsListCmd.Flags().StringVar(&findingsResourceType, "resource-type", "", "Filter by resource type") + + // Findings search flags + securityFindingsSearchCmd.Flags().StringVar(&findingsQuery, "query", "", "Search query using log search syntax (required)") + securityFindingsSearchCmd.Flags().Int32Var(&findingsLimit, "limit", 100, "Maximum results (1-1000)") + securityFindingsSearchCmd.Flags().StringVar(&findingsSort, "sort", "", "Sort field: severity, status, timestamp") + _ = securityFindingsSearchCmd.MarkFlagRequired("query") + + // Command hierarchy securityRulesCmd.AddCommand(securityRulesListCmd, securityRulesGetCmd) securitySignalsCmd.AddCommand(securitySignalsListCmd) - securityFindingsCmd.AddCommand(securityFindingsListCmd) + securityFindingsCmd.AddCommand(securityFindingsListCmd, securityFindingsGetCmd, securityFindingsSearchCmd) securityCmd.AddCommand(securityRulesCmd, securitySignalsCmd, securityFindingsCmd) } @@ -162,18 +246,124 @@ func runSecurityFindingsList(cmd *cobra.Command, args []string) error { } api := datadogV2.NewSecurityMonitoringApi(client.V2()) - resp, r, err := api.ListFindings(client.Context()) + + // Build optional parameters with filtering + opts := datadogV2.ListFindingsOptionalParameters{} + + if findingsPageSize > 0 { + if findingsPageSize > 1000 { + findingsPageSize = 1000 + } + opts.WithPageLimit(findingsPageSize) + } + + if findingsPageCursor != "" { + opts.WithPageCursor(findingsPageCursor) + } + + if findingsStatus != "" { + status, err := datadogV2.NewFindingStatusFromValue(findingsStatus) + if err != nil { + return fmt.Errorf("invalid status value '%s': must be one of critical, high, medium, low, info", findingsStatus) + } + opts.WithFilterStatus(*status) + } + + if findingsEvaluation != "" { + evaluation, err := datadogV2.NewFindingEvaluationFromValue(findingsEvaluation) + if err != nil { + return fmt.Errorf("invalid evaluation value '%s': must be one of pass, fail", findingsEvaluation) + } + opts.WithFilterEvaluation(*evaluation) + } + + if findingsRuleID != "" { + opts.WithFilterRuleId(findingsRuleID) + } + + if findingsResourceType != "" { + opts.WithFilterResourceType(findingsResourceType) + } + + resp, r, err := api.ListFindings(client.Context(), opts) if err != nil { - if r != nil { - return fmt.Errorf("failed to list security findings: %w (status: %d)", err, r.StatusCode) + return formatAPIError("list security findings", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runSecurityFindingsGet(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + findingID := args[0] + api := datadogV2.NewSecurityMonitoringApi(client.V2()) + + resp, r, err := api.GetFinding(client.Context(), findingID) + if err != nil { + return formatAPIError("get security finding", err, r) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + +func runSecurityFindingsSearch(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + api := datadogV2.NewSecurityMonitoringApi(client.V2()) + + // Build search request + searchReq := datadogV2.NewSecurityFindingsSearchRequest() + searchData := datadogV2.NewSecurityFindingsSearchRequestData() + searchAttrs := datadogV2.NewSecurityFindingsSearchRequestDataAttributes() + + // Set filter query + searchAttrs.SetFilter(findingsQuery) + + // Set pagination + if findingsLimit > 0 { + page := datadogV2.NewSecurityFindingsSearchRequestPage() + page.SetLimit(int64(findingsLimit)) + searchAttrs.SetPage(*page) + } + + // Set sort if specified + if findingsSort != "" { + sort, err := datadogV2.NewSecurityFindingsSortFromValue(findingsSort) + if err != nil { + return fmt.Errorf("invalid sort value '%s'", findingsSort) } - return fmt.Errorf("failed to list security findings: %w", err) + searchAttrs.SetSort(*sort) + } + + searchData.SetAttributes(*searchAttrs) + searchReq.SetData(*searchData) + + resp, r, err := api.SearchSecurityFindings(client.Context(), *searchReq) + if err != nil { + return formatAPIError("search security findings", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) if err != nil { return err } - fmt.Println(output) + printOutput("%s\n", output) return nil } diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index bd93a047..e8db8245 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1,6 +1,6 @@ # Command Reference -Complete reference for all 33 command groups in Pup. +Complete reference for all 37 command groups in Pup. ## Command Pattern @@ -23,38 +23,44 @@ pup [options] # Nested commands | metrics | query, list, get, search | cmd/metrics.go | ✅ | | logs | search, list, aggregate | cmd/logs.go | ✅ | | traces | search, list, aggregate | cmd/traces.go | ✅ | -| monitors | list, get, delete | cmd/monitors.go | ✅ | +| monitors | list, get, delete, search | cmd/monitors.go | ✅ | | dashboards | list, get, delete, url | cmd/dashboards.go | ✅ | | slos | list, get, create, update, delete, corrections | cmd/slos.go | ✅ | -| incidents | list, get, create, update | cmd/incidents.go | ✅ | -| rum | apps, metrics, retention-filters, sessions | cmd/rum.go | ⚠️ | -| cicd | pipelines, events | cmd/cicd.go | ⚠️ | -| vulnerabilities | search, list | cmd/vulnerabilities.go | ⚠️ | -| static-analysis | ast, custom-rulesets, sca, coverage | cmd/vulnerabilities.go | ⚠️ | +| incidents | list, get, attachments | cmd/incidents.go | ✅ | +| rum | apps, sessions | cmd/rum.go | ✅ | +| cicd | pipelines, events | cmd/cicd.go | ✅ | +| vulnerabilities | search, list | cmd/vulnerabilities.go | ✅ | +| static-analysis | custom-rulesets | cmd/vulnerabilities.go | ✅ | | downtime | list, get, cancel | cmd/downtime.go | ✅ | -| tags | list, get, add, update, delete | cmd/tags.go | ⚠️ | -| events | list, search, get | cmd/events.go | ⚠️ | -| on-call | teams (list, get) | cmd/on_call.go | ✅ | -| audit-logs | list, search | cmd/audit_logs.go | ⚠️ | +| tags | list, get, add, update, delete | cmd/tags.go | ✅ | +| events | list, search, get | cmd/events.go | ✅ | +| on-call | teams (CRUD, memberships) | cmd/on_call.go | ✅ | +| audit-logs | list, search | cmd/audit_logs.go | ✅ | | api-keys | list, get, create, delete | cmd/api_keys.go | ✅ | +| app-keys | list, get, register, unregister | cmd/app_keys.go | ✅ | | infrastructure | hosts (list, get) | cmd/infrastructure.go | ✅ | | synthetics | tests, locations | cmd/synthetics.go | ✅ | | users | list, get, roles | cmd/users.go | ✅ | | notebooks | list, get, delete | cmd/notebooks.go | ✅ | -| security | rules, signals, findings | cmd/security.go | ✅ | +| security | rules, signals, findings (list, get, search) | cmd/security.go | ✅ | | organizations | get, list | cmd/organizations.go | ✅ | | service-catalog | list, get | cmd/service_catalog.go | ✅ | | error-tracking | issues (list, get) | cmd/error_tracking.go | ✅ | | scorecards | list, get | cmd/scorecards.go | ✅ | -| usage | summary, hourly | cmd/usage.go | ⚠️ | +| usage | summary, hourly | cmd/usage.go | ✅ | +| cost | projected, attribution, by-org | cmd/cost.go | ✅ | +| product-analytics | events send | cmd/product_analytics.go | ✅ | | data-governance | scanner-rules (list) | cmd/data_governance.go | ✅ | | obs-pipelines | list, get | cmd/obs_pipelines.go | ⏳ | | network | flows, devices | cmd/network.go | ⏳ | | cloud | aws, gcp, azure (list) | cmd/cloud.go | ✅ | | integrations | slack, pagerduty, webhooks | cmd/integrations.go | ✅ | | misc | ip-ranges, status | cmd/miscellaneous.go | ✅ | +| cases | create, get, search, assign, archive, projects | cmd/cases.go | ✅ | -**Summary:** 23 working, 7 API-blocked, 3 placeholders +**Summary:** 34 working, 0 API-blocked, 3 placeholders + +**Note:** RUM command (cmd/rum.go) is fully operational. Apps and sessions work completely. Metrics and retention-filters support list/get operations (create/update/delete operations pending due to complex API type structures). ## Common Patterns @@ -135,20 +141,24 @@ pup infrastructure hosts list - **service-catalog** - Service registry (list, get) ### Operations & Incident Response -- **incidents** - Incident management (list, get, create, update) -- **on-call** - On-call teams (teams list, teams get) +- **incidents** - Incident management (list, get, attachments) +- **on-call** - Team management (create, update, delete teams; manage memberships with roles) +- **cases** - Case management (create, search, assign, archive, projects) ### Organization & Access - **users** - User management (list, get, roles) - **organizations** - Org settings (get, list) - **api-keys** - API key management (list, get, create, delete) +- **app-keys** - App key registration for Action Connections (list, get, register, unregister) ### Cost & Usage - **usage** - Usage and billing (summary, hourly) +- **cost** - Cost management (projected, attribution, by-org) ### Configuration & Data Management - **obs-pipelines** - Observability pipelines (list, get) - **misc** - Miscellaneous (ip-ranges, status) +- **product-analytics** - Product analytics events (send) ## Global Flags @@ -162,16 +172,24 @@ Available on all commands: --yes Skip confirmation prompts ``` -## Known API Issues +## Recent Enhancements (v2.54.0 API Client Update) + +The upgrade to datadog-api-client-go v2.54.0 has resolved all previous API blocking issues and added new capabilities: -Commands with ⚠️ status have compilation errors due to datadog-api-client-go library mismatches: +### Newly Unblocked Commands +All 7 previously blocked commands now work: +- ✅ **audit-logs** - Full audit log search and listing +- ✅ **cicd** - CI/CD pipeline visibility and events +- ✅ **events** - Infrastructure event management +- ✅ **tags** - Host tag operations +- ✅ **usage** - Usage and billing metrics +- ✅ **vulnerabilities** - Security vulnerability tracking +- ✅ **static-analysis** - Code security analysis -1. **audit_logs.go** - Pointer method call issue with WithBody -2. **cicd.go** - Method signature mismatches in pipeline events API -3. **events.go** - Missing WithStart/WithEnd methods -4. **rum.go** - Missing ListRUMApplications and metrics API -5. **tags.go** - Type mismatch with Tags field -6. **usage.go** - Missing WithEndHr method, deprecated endpoints -7. **vulnerabilities.go** - Type signature mismatches +### New Command Groups +- ✅ **app-keys** - App key registration for Action Connections and Workflow Automation +- ✅ **cost** - Cost management with projected costs, attribution by tags, and organizational breakdowns +- ✅ **product-analytics** - Send server-side product analytics events with custom properties -These are structural issues in the API client library. Command implementations are correct and will work once the library is updated. +### Enhanced Existing Commands +- **security findings** - Now includes get and search capabilities with advanced filtering (severity, status, resource type, evaluation results)