From b62b08cd9bc7e914a65b3c8e03409eb398f352aa Mon Sep 17 00:00:00 2001 From: Marcos Filipe Date: Fri, 15 May 2026 07:30:23 -0300 Subject: [PATCH] feat: add org management --- cmd/group.go | 164 ++++++++++++++++++++++ cmd/org.go | 301 +++++++++++++++++++++++++++++++++++++++++ cmd/permission.go | 62 +++++++++ cmd/project_member.go | 121 +++++++++++++++++ cmd/service_account.go | 257 +++++++++++++++++++++++++++++++++++ pkg/client/client.go | 6 + 6 files changed, 911 insertions(+) create mode 100644 cmd/group.go create mode 100644 cmd/org.go create mode 100644 cmd/permission.go create mode 100644 cmd/project_member.go create mode 100644 cmd/service_account.go diff --git a/cmd/group.go b/cmd/group.go new file mode 100644 index 0000000..c0f50ad --- /dev/null +++ b/cmd/group.go @@ -0,0 +1,164 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/semaphoreio/sem-ai/pkg/client" + "github.com/semaphoreio/sem-ai/pkg/config" + "github.com/semaphoreio/sem-ai/pkg/output" + "github.com/spf13/cobra" +) + +var groupCmd = &cobra.Command{ + Use: "group", + Short: "Group management — organize members into teams", +} + +var groupListCmd = &cobra.Command{ + Use: "list", + Short: "List groups in the organization", + Example: ` sem-ai group list`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.List("groups") + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var groupCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a group", + Args: cobra.ExactArgs(1), + Example: ` sem-ai group create backend-team + sem-ai group create backend-team --description "Backend engineers" --members "id1,id2"`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]any{ + "name": args[0], + "description": groupDescFlag, + "member_ids": splitCommaList(groupMembersFlag), + } + bodyBytes, _ := json.Marshal(body) + c := client.New() + resp, err := c.Post("groups", bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 && resp.StatusCode != 201 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var groupUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a group — add or remove members", + Args: cobra.ExactArgs(1), + Example: ` sem-ai group update --add "id1,id2" --remove "id3" + sem-ai group update --name "new-name"`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]any{} + if groupUpdateNameFlag != "" { + body["name"] = groupUpdateNameFlag + } + if groupUpdateDescFlag != "" { + body["description"] = groupUpdateDescFlag + } + if groupAddFlag != "" { + body["members_to_add"] = splitCommaList(groupAddFlag) + } + if groupRemoveFlag != "" { + body["members_to_remove"] = splitCommaList(groupRemoveFlag) + } + bodyBytes, _ := json.Marshal(body) + c := client.New() + resp, err := c.Patch("groups", args[0], bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var groupDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a group", + Args: cobra.ExactArgs(1), + Example: ` sem-ai group delete `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.Delete("groups", args[0]) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + output.Result(map[string]string{"status": "deleted", "id": args[0]}) + return nil + }, +} + +var ( + groupDescFlag string + groupMembersFlag string + groupUpdateNameFlag string + groupUpdateDescFlag string + groupAddFlag string + groupRemoveFlag string +) + +func init() { + groupCreateCmd.Flags().StringVar(&groupDescFlag, "description", "", "group description") + groupCreateCmd.Flags().StringVar(&groupMembersFlag, "members", "", "comma-separated member IDs") + + groupUpdateCmd.Flags().StringVar(&groupUpdateNameFlag, "name", "", "new group name") + groupUpdateCmd.Flags().StringVar(&groupUpdateDescFlag, "description", "", "new group description") + groupUpdateCmd.Flags().StringVar(&groupAddFlag, "add", "", "comma-separated member IDs to add") + groupUpdateCmd.Flags().StringVar(&groupRemoveFlag, "remove", "", "comma-separated member IDs to remove") + + groupCmd.AddCommand(groupListCmd) + groupCmd.AddCommand(groupCreateCmd) + groupCmd.AddCommand(groupUpdateCmd) + groupCmd.AddCommand(groupDeleteCmd) + rootCmd.AddCommand(groupCmd) +} diff --git a/cmd/org.go b/cmd/org.go new file mode 100644 index 0000000..ac4e3b8 --- /dev/null +++ b/cmd/org.go @@ -0,0 +1,301 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/semaphoreio/sem-ai/pkg/client" + "github.com/semaphoreio/sem-ai/pkg/config" + "github.com/semaphoreio/sem-ai/pkg/output" + "github.com/spf13/cobra" +) + +var orgCmd = &cobra.Command{ + Use: "org", + Short: "Organization management — members, roles", +} + +var orgMemberCmd = &cobra.Command{ + Use: "member", + Short: "Organization member operations", +} + +var orgMemberListCmd = &cobra.Command{ + Use: "list", + Short: "List organization members", + Example: ` sem-ai org member list`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.List("members") + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var orgRoleCmd = &cobra.Command{ + Use: "role", + Short: "Organization role operations", +} + +var orgRoleListCmd = &cobra.Command{ + Use: "list", + Short: "List organization roles", + Example: ` sem-ai org role list`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.List("roles") + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var orgRoleShowCmd = &cobra.Command{ + Use: "show ", + Short: "Show role details", + Args: cobra.ExactArgs(1), + Example: ` sem-ai org role show `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.Get("roles", args[0]) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var orgRoleCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a custom role", + Args: cobra.ExactArgs(1), + Example: ` sem-ai org role create deployer --permissions "project.view,project.job.rerun" + sem-ai org role create viewer --scope project --permissions "project.view"`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]any{ + "name": args[0], + "description": roleDescFlag, + "scope": roleScopeFlag, + "permissions": splitCommaList(rolePermissionsFlag), + } + bodyBytes, _ := json.Marshal(body) + c := client.New() + resp, err := c.Post("roles", bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 && resp.StatusCode != 201 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var orgRoleUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a custom role", + Args: cobra.ExactArgs(1), + Example: ` sem-ai org role update --permissions "project.view,project.job.rerun"`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]any{} + if roleUpdateNameFlag != "" { + body["name"] = roleUpdateNameFlag + } + if roleUpdateDescFlag != "" { + body["description"] = roleUpdateDescFlag + } + if roleUpdatePermissionsFlag != "" { + body["permissions"] = splitCommaList(roleUpdatePermissionsFlag) + } + bodyBytes, _ := json.Marshal(body) + c := client.New() + resp, err := c.Patch("roles", args[0], bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var orgRoleDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a custom role", + Args: cobra.ExactArgs(1), + Example: ` sem-ai org role delete `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.Delete("roles", args[0]) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + output.Result(map[string]string{"status": "deleted", "id": args[0]}) + return nil + }, +} + +var memberAssignRoleCmd = &cobra.Command{ + Use: "assign-role ", + Short: "Assign an org-level role to a member or service account", + Args: cobra.ExactArgs(2), + Example: ` sem-ai org member assign-role + sem-ai org member assign-role `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]string{"role_id": args[1]} + bodyBytes, _ := json.Marshal(body) + c := client.New() + u := fmt.Sprintf("members/%s/roles", args[0]) + resp, err := c.Post(u, bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var memberRetractRoleCmd = &cobra.Command{ + Use: "retract-role ", + Short: "Remove org-level role from a member or service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai org member retract-role `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + u := fmt.Sprintf("members/%s/roles", args[0]) + resp, err := c.DeletePath(u) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + + +var ( + roleDescFlag string + roleScopeFlag string + rolePermissionsFlag string + roleUpdateNameFlag string + roleUpdateDescFlag string + roleUpdatePermissionsFlag string +) + +func splitCommaList(s string) []string { + if s == "" { + return []string{} + } + var result []string + for _, p := range strings.Split(s, ",") { + p = strings.TrimSpace(p) + if p != "" { + result = append(result, p) + } + } + return result +} + +func init() { + orgRoleCreateCmd.Flags().StringVar(&roleDescFlag, "description", "", "role description") + orgRoleCreateCmd.Flags().StringVar(&roleScopeFlag, "scope", "org", "role scope: org or project") + orgRoleCreateCmd.Flags().StringVar(&rolePermissionsFlag, "permissions", "", "comma-separated permissions") + + orgRoleUpdateCmd.Flags().StringVar(&roleUpdateNameFlag, "name", "", "new role name") + orgRoleUpdateCmd.Flags().StringVar(&roleUpdateDescFlag, "description", "", "new role description") + orgRoleUpdateCmd.Flags().StringVar(&roleUpdatePermissionsFlag, "permissions", "", "comma-separated permissions") + + orgMemberCmd.AddCommand(orgMemberListCmd) + orgRoleCmd.AddCommand(orgRoleListCmd) + orgRoleCmd.AddCommand(orgRoleShowCmd) + orgRoleCmd.AddCommand(orgRoleCreateCmd) + orgRoleCmd.AddCommand(orgRoleUpdateCmd) + orgRoleCmd.AddCommand(orgRoleDeleteCmd) + orgMemberCmd.AddCommand(memberAssignRoleCmd) + orgMemberCmd.AddCommand(memberRetractRoleCmd) + orgCmd.AddCommand(orgMemberCmd) + orgCmd.AddCommand(orgRoleCmd) + rootCmd.AddCommand(orgCmd) +} diff --git a/cmd/permission.go b/cmd/permission.go new file mode 100644 index 0000000..76ab3d0 --- /dev/null +++ b/cmd/permission.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/semaphoreio/sem-ai/pkg/client" + "github.com/semaphoreio/sem-ai/pkg/config" + "github.com/semaphoreio/sem-ai/pkg/output" + "github.com/spf13/cobra" +) + +var permissionCmd = &cobra.Command{ + Use: "permission", + Short: "Permission operations", +} + +var permissionScopeFlag string + +var permissionListCmd = &cobra.Command{ + Use: "list", + Short: "List available permissions", + Example: ` sem-ai permission list + sem-ai permission list --scope project`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + params := url.Values{} + if permissionScopeFlag != "" { + params.Set("scope", permissionScopeFlag) + } + var resp *client.Response + var err error + if len(params) > 0 { + resp, err = c.ListWithParams("permissions", params) + } else { + resp, err = c.List("permissions") + } + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +func init() { + permissionListCmd.Flags().StringVar(&permissionScopeFlag, "scope", "", "filter by scope: org or project") + + permissionCmd.AddCommand(permissionListCmd) + rootCmd.AddCommand(permissionCmd) +} diff --git a/cmd/project_member.go b/cmd/project_member.go new file mode 100644 index 0000000..5af4102 --- /dev/null +++ b/cmd/project_member.go @@ -0,0 +1,121 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/semaphoreio/sem-ai/pkg/client" + "github.com/semaphoreio/sem-ai/pkg/config" + "github.com/semaphoreio/sem-ai/pkg/output" + "github.com/spf13/cobra" +) + +var projectMemberCmd = &cobra.Command{ + Use: "member", + Short: "Project member operations", +} + +var projectMemberListCmd = &cobra.Command{ + Use: "list ", + Short: "List project members", + Args: cobra.ExactArgs(1), + Example: ` sem-ai project member list my-project`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + projectID, err := resolveProjectID(args[0]) + if err != nil { + output.Error("project_error", err.Error(), 1) + return err + } + c := client.New() + u := fmt.Sprintf("projects/%s/members", projectID) + resp, err := c.List(u) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var projectMemberAssignRoleCmd = &cobra.Command{ + Use: "assign-role ", + Short: "Assign a project-level role to a member", + Args: cobra.ExactArgs(3), + Example: ` sem-ai project member assign-role my-project `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + projectID, err := resolveProjectID(args[0]) + if err != nil { + output.Error("project_error", err.Error(), 1) + return err + } + body := map[string]string{"role_id": args[2]} + bodyBytes, _ := json.Marshal(body) + c := client.New() + u := fmt.Sprintf("projects/%s/members/%s/roles", projectID, args[1]) + resp, err := c.Post(u, bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var projectMemberRetractRoleCmd = &cobra.Command{ + Use: "retract-role ", + Short: "Remove project-level role from a member", + Args: cobra.ExactArgs(2), + Example: ` sem-ai project member retract-role my-project `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + projectID, err := resolveProjectID(args[0]) + if err != nil { + output.Error("project_error", err.Error(), 1) + return err + } + c := client.New() + u := fmt.Sprintf("projects/%s/members/%s/roles", projectID, args[1]) + resp, err := c.DeletePath(u) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +func init() { + projectMemberCmd.AddCommand(projectMemberListCmd) + projectMemberCmd.AddCommand(projectMemberAssignRoleCmd) + projectMemberCmd.AddCommand(projectMemberRetractRoleCmd) + projectCmd.AddCommand(projectMemberCmd) +} diff --git a/cmd/service_account.go b/cmd/service_account.go new file mode 100644 index 0000000..8e8b5be --- /dev/null +++ b/cmd/service_account.go @@ -0,0 +1,257 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/semaphoreio/sem-ai/pkg/client" + "github.com/semaphoreio/sem-ai/pkg/config" + "github.com/semaphoreio/sem-ai/pkg/output" + "github.com/spf13/cobra" +) + +var serviceAccountCmd = &cobra.Command{ + Use: "service-account", + Short: "Service account management — create limited-permission tokens", +} + +var serviceAccountListCmd = &cobra.Command{ + Use: "list", + Short: "List service accounts in the organization", + Example: ` sem-ai service-account list`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.List("service_accounts") + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var serviceAccountCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account create ci-bot + sem-ai service-account create ci-bot --description "Bot for CI pipelines"`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]string{ + "name": args[0], + "description": saCreateDescFlag, + } + bodyBytes, _ := json.Marshal(body) + c := client.New() + resp, err := c.Post("service_accounts", bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 && resp.StatusCode != 201 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var serviceAccountShowCmd = &cobra.Command{ + Use: "show ", + Short: "Show service account details", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account show `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.Get("service_accounts", args[0]) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var serviceAccountUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account update --name "new-name" + sem-ai service-account update --description "new description"`, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + body := map[string]string{} + if saUpdateNameFlag != "" { + body["name"] = saUpdateNameFlag + } + if saUpdateDescFlag != "" { + body["description"] = saUpdateDescFlag + } + bodyBytes, _ := json.Marshal(body) + c := client.New() + resp, err := c.Patch("service_accounts", args[0], bodyBytes) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var serviceAccountDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account delete `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + resp, err := c.Delete("service_accounts", args[0]) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + output.Result(map[string]string{"status": "deleted", "id": args[0]}) + return nil + }, +} + +var serviceAccountDeactivateCmd = &cobra.Command{ + Use: "deactivate ", + Short: "Deactivate a service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account deactivate `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + u := fmt.Sprintf("service_accounts/%s/deactivate", args[0]) + resp, err := c.Post(u, nil) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + output.Result(map[string]string{"status": "deactivated", "id": args[0]}) + return nil + }, +} + +var serviceAccountReactivateCmd = &cobra.Command{ + Use: "reactivate ", + Short: "Reactivate a service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account reactivate `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + u := fmt.Sprintf("service_accounts/%s/reactivate", args[0]) + resp, err := c.Post(u, nil) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + output.Result(map[string]string{"status": "reactivated", "id": args[0]}) + return nil + }, +} + +var serviceAccountRegenerateTokenCmd = &cobra.Command{ + Use: "regenerate-token ", + Short: "Regenerate API token for a service account", + Args: cobra.ExactArgs(1), + Example: ` sem-ai service-account regenerate-token `, + RunE: func(cmd *cobra.Command, args []string) error { + if !config.IsConfigured() { + return fmt.Errorf("not configured — run 'sem-ai connect' first") + } + c := client.New() + u := fmt.Sprintf("service_accounts/%s/regenerate_token", args[0]) + resp, err := c.Post(u, nil) + if err != nil { + output.Error("api_error", err.Error(), 1) + return err + } + if resp.StatusCode != 200 { + output.Error("api_error", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(resp.Body)), resp.StatusCode) + return fmt.Errorf("API returned %d", resp.StatusCode) + } + var result any + json.Unmarshal(resp.Body, &result) + output.Result(result) + return nil + }, +} + +var ( + saCreateDescFlag string + saUpdateNameFlag string + saUpdateDescFlag string +) + +func init() { + serviceAccountCreateCmd.Flags().StringVar(&saCreateDescFlag, "description", "", "service account description") + serviceAccountUpdateCmd.Flags().StringVar(&saUpdateNameFlag, "name", "", "new service account name") + serviceAccountUpdateCmd.Flags().StringVar(&saUpdateDescFlag, "description", "", "new service account description") + + serviceAccountCmd.AddCommand(serviceAccountListCmd) + serviceAccountCmd.AddCommand(serviceAccountCreateCmd) + serviceAccountCmd.AddCommand(serviceAccountShowCmd) + serviceAccountCmd.AddCommand(serviceAccountUpdateCmd) + serviceAccountCmd.AddCommand(serviceAccountDeleteCmd) + serviceAccountCmd.AddCommand(serviceAccountDeactivateCmd) + serviceAccountCmd.AddCommand(serviceAccountReactivateCmd) + serviceAccountCmd.AddCommand(serviceAccountRegenerateTokenCmd) + rootCmd.AddCommand(serviceAccountCmd) +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 066d815..80dfeee 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -168,6 +168,12 @@ func (c *Client) Delete(kind, id string) (*Response, error) { return c.doWithRetry("DELETE", u, nil) } +// DeletePath sends a DELETE request to a custom path under /api/{version}/. +func (c *Client) DeletePath(path string) (*Response, error) { + u := fmt.Sprintf("https://%s/api/%s/%s", c.host, c.apiVersion, path) + return c.doWithRetry("DELETE", u, nil) +} + // DeleteWithParams sends a DELETE request with query parameters. func (c *Client) DeleteWithParams(kind, id string, params url.Values) (*Response, error) { u := fmt.Sprintf("https://%s/api/%s/%s/%s?%s", c.host, c.apiVersion, kind, id, params.Encode())