feat: cli-folders-cmd

This commit is contained in:
quinton
2023-11-25 09:44:51 +00:00
parent 84e32faac9
commit b212681d09
6 changed files with 567 additions and 0 deletions

View File

@@ -260,6 +260,82 @@ 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=%s]", err)
}
if response.IsError() {
return GetFoldersV1Response{}, fmt.Errorf("CallGetFoldersV1: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [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. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return folderResponse, nil
}
func CallDeleteFolderV1(httpClient *resty.Client, request DeleteFolderV1Request) (DeleteFolderV1Response, error) {
var folderResponse DeleteFolderV1Response
type deleteFolderRequest struct {
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Directory string `json:"folderPath"`
}
body := deleteFolderRequest{
WorkspaceId: request.WorkspaceId,
Environment: request.Environment,
Directory: request.Directory,
}
httpRequest := httpClient.
R().
SetResult(&folderResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(body)
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. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
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"`

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

@@ -0,0 +1,191 @@
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",
DisableFlagsInUseLine: true,
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)
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",
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
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")
}
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,
InfisicalToken: infisicalToken}
folder, err := util.CreateFolder(params)
if err != nil {
util.HandleError(err, "Unable to create folder")
}
folders := []models.SingleFolder{folder}
visualize.PrintAllFoldersDetails(folders)
Telemetry.CaptureEvent("cli-command:folders create", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a folder",
Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
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")
}
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,
InfisicalToken: infisicalToken,
}
folders, err := util.DeleteFolder(params)
if err != nil {
util.HandleError(err, "Unable to delete folder")
}
visualize.PrintAllFoldersDetails(folders)
Telemetry.CaptureEvent("cli-command:folders delete", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}
func init() {
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", "/", "Path to the directory whose folders will be fetched")
getCmd.Flags().StringP("token", "t", "", "Fetch folders using the infisical token")
folderCmd.AddCommand(getCmd)
// Add createCmd flags here
createCmd.Flags().StringP("path", "p", "/", "Path to the directory where the folder will be created")
createCmd.Flags().StringP("token", "t", "", "Create folder using the infisical token")
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 directory where the folder will be deleted")
deleteCmd.Flags().StringP("token", "t", "", "Delete folder using the infisical token")
deleteCmd.Flags().StringP("name", "n", "", "Name of the folder to be deleted")
folderCmd.AddCommand(deleteCmd)
rootCmd.AddCommand(folderCmd)
}

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,217 @@
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)
}
isConnected := CheckIsConnectedToInternet()
var foldersToReturn []models.SingleFolder
var folderErr error
if params.InfisicalToken == "" {
if isConnected {
log.Debug().Msg("GetAllFolders: Connected to internet, checking logged in creds")
RequireLocalWorkspaceFile()
RequireLogin()
}
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets 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,
}
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) {
rows := [][3]string{}
for _, folder := range folders {
rows = append(rows, [...]string{folder.ID, folder.Name, ""})
}
headers := [...]string{"FOLDER ID", "FOLDER NAME", "VERSION"}
Table(headers, rows)
}