Skip to content

Commit 4383402

Browse files
committed
feat: add interactive 'minder profile edit' command
Signed-off-by: DharunMR <maddharun56@gmail.com>
1 parent fef4137 commit 4383402

1 file changed

Lines changed: 188 additions & 0 deletions

File tree

cmd/cli/app/profile/edit.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package profile
5+
6+
import (
7+
"bytes"
8+
"cmp"
9+
"context"
10+
"fmt"
11+
"os"
12+
"os/exec"
13+
"time"
14+
15+
"github.com/spf13/cobra"
16+
"github.com/spf13/viper"
17+
"google.golang.org/grpc"
18+
"google.golang.org/protobuf/proto"
19+
20+
"github.com/mindersec/minder/internal/util"
21+
"github.com/mindersec/minder/internal/util/cli"
22+
minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
23+
)
24+
25+
var editCmd = &cobra.Command{
26+
Use: "edit",
27+
Short: "Edit an existing profile",
28+
Long: `The profile edit subcommand lets you fetch an existing profile, edit it in your $EDITOR, and apply the updates.`,
29+
RunE: cli.GRPCClientWrapRunE(editCommand),
30+
}
31+
32+
func editCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error {
33+
client := minderv1.NewProfileServiceClient(conn)
34+
project := viper.GetString("project")
35+
id := viper.GetString("id")
36+
name := viper.GetString("name")
37+
38+
if id == "" && name == "" {
39+
return cli.MessageAndError("Error editing profile", fmt.Errorf("id or name required"))
40+
}
41+
cmd.SilenceUsage = true
42+
43+
prof, err := getProfile(ctx, client, project, id, name)
44+
if err != nil {
45+
return err
46+
}
47+
48+
// hardcoded type and version since ParseResource requires it
49+
if prof.Type == "" {
50+
prof.Type = "profile"
51+
}
52+
if prof.Version == "" {
53+
prof.Version = "v1"
54+
}
55+
56+
yamlString, err := util.GetYamlFromProto(prof)
57+
if err != nil {
58+
return cli.MessageAndError("Error marshaling profile to YAML", err)
59+
}
60+
61+
tmpFile, err := os.CreateTemp("", "tmp-minder-profile-*.yaml")
62+
if err != nil {
63+
return cli.MessageAndError("Error creating temporary file", err)
64+
}
65+
defer os.Remove(tmpFile.Name())
66+
67+
if _, err := tmpFile.WriteString(yamlString); err != nil {
68+
return cli.MessageAndError("Error writing to temporary file", err)
69+
}
70+
71+
// we must close the file descriptor before handing it to the editor
72+
// many terminal editor perform atomic saves which changes the file's inode
73+
// keeping the old FD open would read the orphaned file.
74+
if err := tmpFile.Close(); err != nil {
75+
return cli.MessageAndError("Error closing temporary file", err)
76+
}
77+
78+
if err := handleEditor(tmpFile.Name()); err != nil {
79+
return err
80+
}
81+
82+
updatedBytes, err := os.ReadFile(tmpFile.Name())
83+
if err != nil {
84+
return cli.MessageAndError("Error reading updated temporary file", err)
85+
}
86+
87+
if string(updatedBytes) == yamlString {
88+
cmd.Println("No changes made to the profile. Aborting update.")
89+
return nil
90+
}
91+
92+
return updateProfile(ctx, client, prof, updatedBytes, cmd)
93+
}
94+
95+
func getProfile(ctx context.Context, client minderv1.ProfileServiceClient, project, id, name string) (*minderv1.Profile, error) {
96+
if id != "" {
97+
p, err := client.GetProfileById(ctx, &minderv1.GetProfileByIdRequest{
98+
Context: &minderv1.Context{Project: &project},
99+
Id: id,
100+
})
101+
if err != nil {
102+
return nil, cli.MessageAndError("Error getting profile by ID", err)
103+
}
104+
return p.GetProfile(), nil
105+
}
106+
107+
p, err := client.GetProfileByName(ctx, &minderv1.GetProfileByNameRequest{
108+
Context: &minderv1.Context{Project: &project},
109+
Name: name,
110+
})
111+
if err != nil {
112+
return nil, cli.MessageAndError("Error getting profile by name", err)
113+
}
114+
return p.GetProfile(), nil
115+
}
116+
117+
func handleEditor(fileName string) error {
118+
editorCmd := cmp.Or(os.Getenv("VISUAL"), os.Getenv("EDITOR"))
119+
120+
if editorCmd == "" {
121+
commonEditors := []string{"nano", "vim", "nvim", "vi", "emacs"}
122+
for _, e := range commonEditors {
123+
if _, err := exec.LookPath(e); err == nil {
124+
editorCmd = e
125+
break
126+
}
127+
}
128+
}
129+
130+
if editorCmd == "" {
131+
msg := "no editor found in $PATH. Please set $EDITOR to your preferred editor"
132+
return cli.MessageAndError(msg, fmt.Errorf("no editor found"))
133+
}
134+
135+
// #nosec G204
136+
// #nosec G702
137+
execCmd := exec.Command(editorCmd, fileName)
138+
execCmd.Stdin, execCmd.Stdout, execCmd.Stderr = os.Stdin, os.Stdout, os.Stderr
139+
140+
if err := execCmd.Run(); err != nil {
141+
return cli.MessageAndError(fmt.Sprintf("Editor execution failed (%s)", editorCmd), err)
142+
}
143+
return nil
144+
}
145+
146+
func updateProfile(
147+
_ context.Context,
148+
client minderv1.ProfileServiceClient,
149+
oldProf *minderv1.Profile,
150+
updatedBytes []byte,
151+
cmd *cobra.Command,
152+
) error {
153+
var updatedProfile minderv1.Profile
154+
if err := minderv1.ParseResource(bytes.NewReader(updatedBytes), &updatedProfile); err != nil {
155+
return cli.MessageAndError("Error parsing updated profile YAML", err)
156+
}
157+
158+
updatedProfile.Id = proto.String(oldProf.GetId())
159+
updatedProfile.Context = oldProf.GetContext()
160+
updatedProfile.Type = oldProf.GetType()
161+
if updatedProfile.Type == "" {
162+
updatedProfile.Type = "profile"
163+
}
164+
updatedProfile.Version = oldProf.GetVersion()
165+
if updatedProfile.Version == "" {
166+
updatedProfile.Version = "v1"
167+
}
168+
169+
updateCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
170+
defer cancel()
171+
172+
resp, err := client.UpdateProfile(updateCtx, &minderv1.UpdateProfileRequest{
173+
Profile: &updatedProfile,
174+
})
175+
if err != nil {
176+
return cli.MessageAndError("Error updating profile", err)
177+
}
178+
179+
cmd.Println("Successfully updated profile named:", resp.GetProfile().GetName())
180+
return nil
181+
}
182+
183+
func init() {
184+
ProfileCmd.AddCommand(editCmd)
185+
editCmd.Flags().StringP("id", "i", "", "ID of the profile to edit")
186+
editCmd.Flags().StringP("name", "n", "", "Name of the profile to edit")
187+
editCmd.MarkFlagsMutuallyExclusive("id", "name")
188+
}

0 commit comments

Comments
 (0)