Merge pull request #1193 from quinton11/feat/cli-folder-cmd

feat: cli-folders-cmd
This commit is contained in:
Maidul Islam
2023-11-28 19:31:04 -05:00
committed by GitHub
7 changed files with 545 additions and 0 deletions

View File

@@ -261,6 +261,70 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
return secretsResponse, nil
}
func CallGetFoldersV1(httpClient *resty.Client, request GetFoldersV1Request) (GetFoldersV1Response, error) {
var foldersResponse GetFoldersV1Response
httpRequest := httpClient.
R().
SetResult(&foldersResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("directory", request.FoldersPath)
response, err := httpRequest.Get(fmt.Sprintf("%v/v1/folders", config.INFISICAL_URL))
if err != nil {
return GetFoldersV1Response{}, fmt.Errorf("CallGetFoldersV1: Unable to complete api request [err=%v]", err)
}
if response.IsError() {
return GetFoldersV1Response{}, fmt.Errorf("CallGetFoldersV1: Unsuccessful [response=%s]", response)
}
return foldersResponse, nil
}
func CallCreateFolderV1(httpClient *resty.Client, request CreateFolderV1Request) (CreateFolderV1Response, error) {
var folderResponse CreateFolderV1Response
httpRequest := httpClient.
R().
SetResult(&folderResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request)
response, err := httpRequest.Post(fmt.Sprintf("%v/v1/folders", config.INFISICAL_URL))
if err != nil {
return CreateFolderV1Response{}, fmt.Errorf("CallCreateFolderV1: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return CreateFolderV1Response{}, fmt.Errorf("CallCreateFolderV1: Unsuccessful [response=%s]", response.String())
}
return folderResponse, nil
}
func CallDeleteFolderV1(httpClient *resty.Client, request DeleteFolderV1Request) (DeleteFolderV1Response, error) {
var folderResponse DeleteFolderV1Response
httpRequest := httpClient.
R().
SetResult(&folderResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request)
response, err := httpRequest.Delete(fmt.Sprintf("%v/v1/folders/%v", config.INFISICAL_URL, request.FolderName))
if err != nil {
return DeleteFolderV1Response{}, fmt.Errorf("CallDeleteFolderV1: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return DeleteFolderV1Response{}, fmt.Errorf("CallDeleteFolderV1: Unsuccessful [response=%s]", response.String())
}
return folderResponse, nil
}
func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.

View File

@@ -278,6 +278,47 @@ type GetEncryptedSecretsV3Request struct {
IncludeImport bool `json:"include_imports"`
}
type GetFoldersV1Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
FoldersPath string `json:"foldersPath"`
}
type GetFoldersV1Response struct {
Folders []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"folders"`
}
type CreateFolderV1Request struct {
FolderName string `json:"folderName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Directory string `json:"directory"`
}
type CreateFolderV1Response struct {
Folder struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"folder"`
}
type DeleteFolderV1Request struct {
FolderName string `json:"folderName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Directory string `json:"directory"`
}
type DeleteFolderV1Response struct {
Folders []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"folders"`
}
type EncryptedSecretV3 struct {
ID string `json:"_id"`
Version int `json:"version"`

164
cli/packages/cmd/folder.go Normal file
View File

@@ -0,0 +1,164 @@
package cmd
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize"
"github.com/posthog/posthog-go"
"github.com/spf13/cobra"
)
var folderCmd = &cobra.Command{
Use: "folders",
Short: "Create, delete, and list folders",
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
var getCmd = &cobra.Command{
Use: "get",
Short: "Get folders in a directory",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
util.RequireLogin()
},
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
foldersPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
folders, err := util.GetAllFolders(models.GetAllFoldersParameters{Environment: environmentName, InfisicalToken: infisicalToken, FoldersPath: foldersPath})
if err != nil {
util.HandleError(err, "Unable to get folders")
}
visualize.PrintAllFoldersDetails(folders, foldersPath)
Telemetry.CaptureEvent("cli-command:folders get", posthog.NewProperties().Set("folderCount", len(folders)).Set("version", util.CLI_VERSION))
},
}
var createCmd = &cobra.Command{
Use: "create",
Short: "Create a folder",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
},
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
folderPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
folderName, err := cmd.Flags().GetString("name")
if err != nil {
util.HandleError(err, "Unable to parse name flag")
}
if folderName == "" {
util.HandleError(fmt.Errorf("Invalid folder name"), "Folder name cannot be empty")
}
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "Unable to get workspace file")
}
params := models.CreateFolderParameters{
FolderName: folderName,
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
FolderPath: folderPath,
}
_, err = util.CreateFolder(params)
if err != nil {
util.HandleError(err, "Unable to create folder")
}
util.PrintSuccessMessage(fmt.Sprintf("folder named `%s` created in path %s", folderName, folderPath))
Telemetry.CaptureEvent("cli-command:folders create", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a folder",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
},
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
folderPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
folderName, err := cmd.Flags().GetString("name")
if err != nil {
util.HandleError(err, "Unable to parse name flag")
}
if folderName == "" {
util.HandleError(fmt.Errorf("Invalid folder name"), "Folder name cannot be empty")
}
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "Unable to get workspace file")
}
params := models.DeleteFolderParameters{
FolderName: folderName,
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
FolderPath: folderPath,
}
_, err = util.DeleteFolder(params)
if err != nil {
util.HandleError(err, "Unable to delete folder")
}
util.PrintSuccessMessage(fmt.Sprintf("folder named `%s` deleted in path %s", folderName, folderPath))
Telemetry.CaptureEvent("cli-command:folders delete", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}

View File

@@ -679,6 +679,28 @@ func init() {
util.RequireLocalWorkspaceFile()
}
// *** Folders sub command ***
folderCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
// Add getCmd, createCmd and deleteCmd flags here
getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from")
getCmd.Flags().String("token", "", "Fetch folders using the infisical token")
folderCmd.AddCommand(getCmd)
// Add createCmd flags here
createCmd.Flags().StringP("path", "p", "/", "Path to where the folder should be created")
createCmd.Flags().StringP("name", "n", "", "Name of the folder to be created")
folderCmd.AddCommand(createCmd)
// Add deleteCmd flags here
deleteCmd.Flags().StringP("path", "p", "/", "Path to the folder to be deleted")
deleteCmd.Flags().StringP("name", "n", "", "Name of the folder to be deleted")
folderCmd.AddCommand(deleteCmd)
secretsCmd.AddCommand(folderCmd)
// ** End of folders sub command
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")

View File

@@ -34,6 +34,11 @@ type SingleEnvironmentVariable struct {
Comment string `json:"comment"`
}
type SingleFolder struct {
ID string `json:"_id"`
Name string `json:"name"`
}
type Workspace struct {
ID string `json:"_id"`
Name string `json:"name"`
@@ -63,3 +68,26 @@ type GetAllSecretsParameters struct {
SecretsPath string
IncludeImport bool
}
type GetAllFoldersParameters struct {
WorkspaceId string
Environment string
FoldersPath string
InfisicalToken string
}
type CreateFolderParameters struct {
FolderName string
WorkspaceId string
Environment string
FolderPath string
InfisicalToken string
}
type DeleteFolderParameters struct {
FolderName string
WorkspaceId string
Environment string
FolderPath string
InfisicalToken string
}

View File

@@ -0,0 +1,212 @@
package util
import (
"fmt"
"os"
"strings"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
)
func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder, error) {
if params.InfisicalToken == "" {
params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
}
var foldersToReturn []models.SingleFolder
var folderErr error
if params.InfisicalToken == "" {
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
return nil, err
}
if loggedInUserDetails.LoginExpired {
PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
workspaceFile, err := GetWorkSpaceFromFile()
if err != nil {
return nil, err
}
if params.WorkspaceId != "" {
workspaceFile.WorkspaceId = params.WorkspaceId
}
folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, workspaceFile.WorkspaceId, params.Environment, params.FoldersPath)
folderErr = err
foldersToReturn = folders
} else {
// get folders via service token
folders, err := GetFoldersViaServiceToken(params.InfisicalToken, params.WorkspaceId, params.Environment, params.FoldersPath)
folderErr = err
foldersToReturn = folders
}
return foldersToReturn, folderErr
}
func GetFoldersViaJTW(JTWToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) {
// set up resty client
httpClient := resty.New()
httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
getFoldersRequest := api.GetFoldersV1Request{
WorkspaceId: workspaceId,
Environment: environmentName,
FoldersPath: foldersPath,
}
apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest)
if err != nil {
return nil, err
}
var folders []models.SingleFolder
for _, folder := range apiResponse.Folders {
folders = append(folders, models.SingleFolder{
Name: folder.Name,
ID: folder.ID,
})
}
return folders, nil
}
func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
httpClient := resty.New()
httpClient.SetAuthToken(serviceToken).
SetHeader("Accept", "application/json")
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil {
return nil, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
// if multiple scopes are there then user needs to specify which environment and folder path
if environmentName == "" {
if len(serviceTokenDetails.Scopes) != 1 {
return nil, fmt.Errorf("you need to provide the --env for multiple environment scoped token")
} else {
environmentName = serviceTokenDetails.Scopes[0].Environment
}
}
getFoldersRequest := api.GetFoldersV1Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: environmentName,
FoldersPath: foldersPath,
}
apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest)
if err != nil {
return nil, fmt.Errorf("unable to get folders. [err=%v]", err)
}
var folders []models.SingleFolder
for _, folder := range apiResponse.Folders {
folders = append(folders, models.SingleFolder{
Name: folder.Name,
ID: folder.ID,
})
}
return folders, nil
}
// CreateFolder creates a folder in Infisical
func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) {
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
return models.SingleFolder{}, err
}
if loggedInUserDetails.LoginExpired {
PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
// set up resty client
httpClient := resty.New()
httpClient.
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json")
createFolderRequest := api.CreateFolderV1Request{
WorkspaceId: params.WorkspaceId,
Environment: params.Environment,
FolderName: params.FolderName,
Directory: params.FolderPath,
}
apiResponse, err := api.CallCreateFolderV1(httpClient, createFolderRequest)
if err != nil {
return models.SingleFolder{}, err
}
folder := apiResponse.Folder
return models.SingleFolder{
Name: folder.Name,
ID: folder.ID,
}, nil
}
func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, error) {
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
return nil, err
}
if loggedInUserDetails.LoginExpired {
PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
// set up resty client
httpClient := resty.New()
httpClient.
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json")
deleteFolderRequest := api.DeleteFolderV1Request{
WorkspaceId: params.WorkspaceId,
Environment: params.Environment,
FolderName: params.FolderName,
Directory: params.FolderPath,
}
apiResponse, err := api.CallDeleteFolderV1(httpClient, deleteFolderRequest)
if err != nil {
return nil, err
}
var folders []models.SingleFolder
for _, folder := range apiResponse.Folders {
folders = append(folders, models.SingleFolder{
Name: folder.Name,
ID: folder.ID,
})
}
return folders, nil
}

View File

@@ -0,0 +1,14 @@
package visualize
import "github.com/Infisical/infisical-merge/packages/models"
func PrintAllFoldersDetails(folders []models.SingleFolder, path string) {
rows := [][3]string{}
for _, folder := range folders {
rows = append(rows, [...]string{folder.Name, path, folder.ID})
}
headers := [...]string{"FOLDER NAME", "PATH", "FOLDER ID"}
Table(headers, rows)
}