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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions cmd/cli/app/profile/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
// SPDX-License-Identifier: Apache-2.0

package profile

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"sigs.k8s.io/yaml"

"github.com/mindersec/minder/internal/util"
"github.com/mindersec/minder/internal/util/cli"
minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
)

var editCmd = &cobra.Command{
Use: "edit",
Short: "Edit an existing profile",
Long: `The profile edit subcommand lets you fetch an existing profile, edit it in your $EDITOR, and apply the updates.`,
RunE: cli.GRPCClientWrapRunE(editCommand),
}

func editCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error {
client := minderv1.NewProfileServiceClient(conn)
project := viper.GetString("project")
id := viper.GetString("id")
name := viper.GetString("name")

if id == "" && name == "" {
return cli.MessageAndError("Error editing profile", fmt.Errorf("id or name required"))
}
cmd.SilenceUsage = true

prof, err := getProfile(ctx, client, project, id, name)
if err != nil {
return err
}

yamlString, err := util.GetYamlFromProto(prof)
if err != nil {
return cli.MessageAndError("Error marshaling profile to YAML", err)
}

tmpFile, err := os.CreateTemp("", "tmp-minder-profile-*.yaml")
if err != nil {
return cli.MessageAndError("Error creating temporary file", err)
}
defer os.Remove(tmpFile.Name())

if _, err := tmpFile.WriteString(yamlString); err != nil {
return cli.MessageAndError("Error writing to temporary file", err)
}
if err := tmpFile.Close(); err != nil {
return cli.MessageAndError("Error closing temporary file", err)
}

if err := handleEditor(tmpFile.Name()); err != nil {
return err
}

updatedBytes, err := os.ReadFile(tmpFile.Name())
if err != nil {
return cli.MessageAndError("Error reading updated temporary file", err)
}

if string(updatedBytes) == yamlString {
cmd.Println("No changes made to the profile. Aborting update.")
return nil
}

return updateProfile(ctx, client, prof, updatedBytes, cmd)
}

func getProfile(ctx context.Context, client minderv1.ProfileServiceClient, project, id, name string) (*minderv1.Profile, error) {
if id != "" {
p, err := client.GetProfileById(ctx, &minderv1.GetProfileByIdRequest{
Context: &minderv1.Context{Project: &project},
Id: id,
})
if err != nil {
return nil, cli.MessageAndError("Error getting profile by ID", err)
}
return p.GetProfile(), nil
}

p, err := client.GetProfileByName(ctx, &minderv1.GetProfileByNameRequest{
Context: &minderv1.Context{Project: &project},
Name: name,
})
if err != nil {
return nil, cli.MessageAndError("Error getting profile by name", err)
}
return p.GetProfile(), nil
}

func handleEditor(fileName string) error {
editorCmd := os.Getenv("VISUAL")
if editorCmd == "" {
editorCmd = os.Getenv("EDITOR")
}

if editorCmd == "" {
commonEditors := []string{"nano", "vim", "nvim", "vi", "emacs"}
for _, e := range commonEditors {
if _, err := exec.LookPath(e); err == nil {
editorCmd = e
break
}
}
}

if editorCmd == "" {
msg := "no editor found in $PATH. Please install nano/vim or set $EDITOR"
return cli.MessageAndError(msg, fmt.Errorf("no editor found"))
}

args := strings.Fields(editorCmd)
executable := args[0]
execArgs := append(args[1:], fileName)

// #nosec G204
// #nosec G702
execCmd := exec.Command(executable, execArgs...)
execCmd.Stdin, execCmd.Stdout, execCmd.Stderr = os.Stdin, os.Stdout, os.Stderr

if err := execCmd.Run(); err != nil {
return cli.MessageAndError(fmt.Sprintf("Editor execution failed (%s)", executable), err)
}
return nil
}

func updateProfile(
_ context.Context,
client minderv1.ProfileServiceClient,
oldProf *minderv1.Profile,
updatedBytes []byte,
cmd *cobra.Command,
) error {
var updatedProfile minderv1.Profile
updatedJSON, err := yaml.YAMLToJSON(updatedBytes)
if err != nil {
return cli.MessageAndError("Error parsing updated YAML", err)
}
um := protojson.UnmarshalOptions{DiscardUnknown: true}
if err := um.Unmarshal(updatedJSON, &updatedProfile); err != nil {
return cli.MessageAndError("Error loading updated profile data into protobuf", err)
}

updatedProfile.Id = proto.String(oldProf.GetId())
updatedProfile.Context = oldProf.GetContext()
updatedProfile.Type = oldProf.GetType()
if updatedProfile.Type == "" {
updatedProfile.Type = "profile"
}
updatedProfile.Version = oldProf.GetVersion()
if updatedProfile.Version == "" {
updatedProfile.Version = "v1"
}

updateCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

resp, err := client.UpdateProfile(updateCtx, &minderv1.UpdateProfileRequest{
Profile: &updatedProfile,
})
if err != nil {
return cli.MessageAndError("Error updating profile", err)
}

cmd.Println("Successfully updated profile named:", resp.GetProfile().GetName())
return nil
}

func init() {
ProfileCmd.AddCommand(editCmd)
editCmd.Flags().StringP("id", "i", "", "ID of the profile to edit")
editCmd.Flags().StringP("name", "n", "", "Name of the profile to edit")
editCmd.MarkFlagsMutuallyExclusive("id", "name")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
sigs.k8s.io/yaml v1.6.0
)

// Pin this to 1.2.1 until using containerd/v2; 1.3.0 has a backwards incompatible change
Expand Down
Loading