877 lines
21 KiB
Go
877 lines
21 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/AlecAivazis/survey/v2/core"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/ankitpokhrel/jira-cli/api"
|
|
"github.com/ankitpokhrel/jira-cli/internal/cmdutil"
|
|
"github.com/ankitpokhrel/jira-cli/pkg/jira"
|
|
)
|
|
|
|
const (
|
|
// Dir is a jira-cli config directory.
|
|
Dir = ".jira"
|
|
// FileName is a jira-cli config file name.
|
|
FileName = ".config"
|
|
// FileType is a jira-cli config file extension.
|
|
FileType = "yml"
|
|
|
|
optionSearch = "[Search...]"
|
|
optionBack = "Go-back"
|
|
optionNone = "None"
|
|
lineBreak = "----------"
|
|
)
|
|
|
|
var (
|
|
// ErrSkip is returned when a user skips the config generation.
|
|
ErrSkip = fmt.Errorf("skipping config generation")
|
|
// ErrUnexpectedResponseFormat is returned if the response data is in unexpected format.
|
|
ErrUnexpectedResponseFormat = fmt.Errorf("unexpected response format")
|
|
)
|
|
|
|
// projectConf is a trimmed down version of jira.Project.
|
|
type projectConf struct {
|
|
Key string `json:"key"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// issueTypeFieldConf is a trimmed down version of jira.IssueTypeField.
|
|
type issueTypeFieldConf struct {
|
|
Name string `yaml:"name"`
|
|
Key string `yaml:"key"`
|
|
Schema struct {
|
|
DataType string `yaml:"datatype"`
|
|
Items string `yaml:"items,omitempty"`
|
|
}
|
|
}
|
|
|
|
// JiraCLIMTLSConfig is an authtype specific config.
|
|
type JiraCLIMTLSConfig struct {
|
|
CaCert string
|
|
ClientCert string
|
|
ClientKey string
|
|
}
|
|
|
|
// JiraCLIConfig is a Jira CLI config.
|
|
type JiraCLIConfig struct {
|
|
Installation string
|
|
Server string
|
|
AuthType string
|
|
Login string
|
|
Project string
|
|
Board string
|
|
Force bool
|
|
Insecure bool
|
|
MTLS JiraCLIMTLSConfig
|
|
}
|
|
|
|
// JiraCLIConfigGenerator is a Jira CLI config generator.
|
|
type JiraCLIConfigGenerator struct {
|
|
usrCfg *JiraCLIConfig
|
|
value struct {
|
|
installation string
|
|
server string
|
|
version struct {
|
|
major, minor, patch int
|
|
}
|
|
login string
|
|
authType jira.AuthType
|
|
project *projectConf
|
|
board *jira.Board
|
|
epic *jira.Epic
|
|
issueTypes []*jira.IssueType
|
|
customFields []*issueTypeFieldConf
|
|
mtls struct {
|
|
caCert, clientCert, clientKey string
|
|
}
|
|
timezone string
|
|
}
|
|
jiraClient *jira.Client
|
|
projectSuggestions []string
|
|
boardSuggestions []string
|
|
projectsMap map[string]*projectConf
|
|
boardsMap map[string]*jira.Board
|
|
}
|
|
|
|
// NewJiraCLIConfigGenerator creates a new Jira CLI config.
|
|
func NewJiraCLIConfigGenerator(cfg *JiraCLIConfig) *JiraCLIConfigGenerator {
|
|
gen := JiraCLIConfigGenerator{
|
|
usrCfg: cfg,
|
|
projectsMap: make(map[string]*projectConf),
|
|
boardsMap: make(map[string]*jira.Board),
|
|
}
|
|
|
|
return &gen
|
|
}
|
|
|
|
// Generate generates the config file.
|
|
//
|
|
//nolint:gocyclo
|
|
func (c *JiraCLIConfigGenerator) Generate() (string, error) {
|
|
var cfgFile string
|
|
|
|
if cfgFile = viper.ConfigFileUsed(); cfgFile == "" {
|
|
home, err := cmdutil.GetConfigHome()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cfgFile = fmt.Sprintf("%s/%s/%s.%s", home, Dir, FileName, FileType)
|
|
} else {
|
|
isExtValid := func() bool {
|
|
cf := strings.ToLower(cfgFile)
|
|
for _, ext := range []string{FileType, "yaml"} {
|
|
if strings.HasSuffix(cf, fmt.Sprintf(".%s", ext)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
// Enforce .yml extension.
|
|
if !isExtValid() {
|
|
cfgFile = fmt.Sprintf("%s.%s", cfgFile, FileType)
|
|
}
|
|
}
|
|
|
|
cfgExists := func() bool {
|
|
s := cmdutil.Info("Checking configuration...")
|
|
defer s.Stop()
|
|
|
|
return Exists(cfgFile)
|
|
}()
|
|
|
|
if !c.usrCfg.Force && cfgExists && !shallOverwrite() {
|
|
return "", ErrSkip
|
|
}
|
|
if err := c.configureInstallationType(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if c.value.installation == jira.InstallationTypeLocal {
|
|
if err := c.configureLocalAuthType(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if c.usrCfg.AuthType != "" {
|
|
c.value.authType = jira.AuthType(c.usrCfg.AuthType)
|
|
}
|
|
|
|
if c.value.authType == jira.AuthTypeMTLS {
|
|
if err := c.configureMTLS(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if err := c.configureServerAndLoginDetails(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if c.value.installation == jira.InstallationTypeLocal {
|
|
if err := c.configureServerMeta(c.value.server, c.value.login); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if err := c.configureProjectAndBoardDetails(); err != nil {
|
|
return "", err
|
|
}
|
|
if err := c.configureMetadata(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := func() error {
|
|
s := cmdutil.Info("Creating new configuration...")
|
|
defer s.Stop()
|
|
|
|
return create(cfgFile)
|
|
}(); err != nil {
|
|
return "", err
|
|
}
|
|
return c.write(cfgFile)
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureInstallationType() error {
|
|
switch c.usrCfg.Installation {
|
|
case strings.ToLower(jira.InstallationTypeCloud):
|
|
c.value.installation = jira.InstallationTypeCloud
|
|
case strings.ToLower(jira.InstallationTypeLocal):
|
|
c.value.installation = jira.InstallationTypeLocal
|
|
default:
|
|
qs := &survey.Select{
|
|
Message: "Installation type:",
|
|
Help: "Is this a cloud installation or an on-premise (local) installation.",
|
|
Options: []string{"Cloud", "Local"},
|
|
Default: "Cloud",
|
|
}
|
|
|
|
var installation string
|
|
if err := survey.AskOne(qs, &installation); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.value.installation = installation
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureLocalAuthType() error {
|
|
authType := c.usrCfg.AuthType
|
|
|
|
if c.usrCfg.AuthType == "" {
|
|
qs := &survey.Select{
|
|
Message: "Authentication type:",
|
|
Help: `Authentication type coud be: basic (login), bearer (PAT) or mtls (client certs)
|
|
? If you are using your login credentials, the auth type is probably 'basic' (most common for local installation)
|
|
? If you are using a personal access token, the auth type is probably 'bearer'`,
|
|
Options: []string{"basic", "bearer", "mtls"},
|
|
Default: "basic",
|
|
}
|
|
if err := survey.AskOne(qs, &authType); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
switch authType {
|
|
case jira.AuthTypeBearer.String():
|
|
c.value.authType = jira.AuthTypeBearer
|
|
case jira.AuthTypeMTLS.String():
|
|
c.value.authType = jira.AuthTypeMTLS
|
|
default:
|
|
c.value.authType = jira.AuthTypeBasic
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureMTLS() error {
|
|
var qs []*survey.Question
|
|
|
|
c.value.mtls.caCert = c.usrCfg.MTLS.CaCert
|
|
c.value.mtls.clientCert = c.usrCfg.MTLS.ClientCert
|
|
c.value.mtls.clientKey = c.usrCfg.MTLS.ClientKey
|
|
|
|
getIfEmpty := func(conf, name, msg, help string) {
|
|
if conf != "" {
|
|
return
|
|
}
|
|
qs = append(qs, &survey.Question{
|
|
Name: name,
|
|
Prompt: &survey.Input{
|
|
Message: msg,
|
|
Help: help,
|
|
},
|
|
})
|
|
}
|
|
|
|
getIfEmpty(c.value.mtls.caCert, "cacert", "CA Certificate", "Local path to CA Certificate for your `server`")
|
|
getIfEmpty(c.value.mtls.clientCert, "clientcert", "Client Certificate", "Local path to your client certificate")
|
|
getIfEmpty(c.value.mtls.clientKey, "clientkey", "Client Key", "Local path to your client key")
|
|
|
|
if len(qs) > 0 {
|
|
ans := struct {
|
|
CaCert string
|
|
ClientCert string
|
|
ClientKey string
|
|
}{}
|
|
|
|
if err := survey.Ask(qs, &ans); err != nil {
|
|
return err
|
|
}
|
|
|
|
if ans.CaCert != "" {
|
|
c.value.mtls.caCert = ans.CaCert
|
|
}
|
|
if ans.ClientCert != "" {
|
|
c.value.mtls.clientCert = ans.ClientCert
|
|
}
|
|
if ans.ClientKey != "" {
|
|
c.value.mtls.clientKey = ans.ClientKey
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func (c *JiraCLIConfigGenerator) configureServerAndLoginDetails() error {
|
|
var qs []*survey.Question
|
|
|
|
c.value.server = c.usrCfg.Server
|
|
c.value.login = c.usrCfg.Login
|
|
|
|
if c.usrCfg.Server == "" {
|
|
qs = append(qs, &survey.Question{
|
|
Name: "server",
|
|
Prompt: &survey.Input{
|
|
Message: "Link to Jira server:",
|
|
Help: "This is a link to your jira server, eg: https://company.atlassian.net",
|
|
},
|
|
Validate: func(val interface{}) error {
|
|
errInvalidURL := fmt.Errorf("not a valid URL")
|
|
|
|
str, ok := val.(string)
|
|
if !ok {
|
|
return errInvalidURL
|
|
}
|
|
u, err := url.Parse(str)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
return errInvalidURL
|
|
}
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
return errInvalidURL
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
if c.usrCfg.Login == "" {
|
|
switch c.value.installation {
|
|
case jira.InstallationTypeCloud:
|
|
qs = append(qs, &survey.Question{
|
|
Name: "login",
|
|
Prompt: &survey.Input{
|
|
Message: "Login email:",
|
|
Help: "This is the email you use to login to your jira account.",
|
|
},
|
|
Validate: func(val interface{}) error {
|
|
var (
|
|
emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9]" +
|
|
"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
|
|
|
errInvalidEmail = fmt.Errorf("not a valid email")
|
|
)
|
|
|
|
str, ok := val.(string)
|
|
if !ok {
|
|
return errInvalidEmail
|
|
}
|
|
if len(str) < 3 || len(str) > 254 {
|
|
return errInvalidEmail
|
|
}
|
|
if !emailRegex.MatchString(str) {
|
|
return errInvalidEmail
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
case jira.InstallationTypeLocal:
|
|
qs = append(qs, &survey.Question{
|
|
Name: "login",
|
|
Prompt: &survey.Input{
|
|
Message: "Login username:",
|
|
Help: "This is the username you use to login to your jira account.",
|
|
},
|
|
Validate: func(val interface{}) error {
|
|
errInvalidUser := fmt.Errorf("not a valid user")
|
|
|
|
str, ok := val.(string)
|
|
if !ok {
|
|
return errInvalidUser
|
|
}
|
|
if len(str) < 3 || len(str) > 254 {
|
|
return errInvalidUser
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(qs) > 0 {
|
|
ans := struct {
|
|
Server string
|
|
Login string
|
|
}{}
|
|
|
|
if err := survey.Ask(qs, &ans); err != nil {
|
|
return err
|
|
}
|
|
|
|
if ans.Server != "" {
|
|
c.value.server = ans.Server
|
|
}
|
|
if ans.Login != "" {
|
|
c.value.login = ans.Login
|
|
}
|
|
}
|
|
|
|
return c.verifyLoginDetails(c.value.server, c.value.login)
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) verifyLoginDetails(server, login string) error {
|
|
s := cmdutil.Info("Verifying login details...")
|
|
defer s.Stop()
|
|
|
|
server = strings.TrimRight(server, "/")
|
|
|
|
c.jiraClient = api.Client(jira.Config{
|
|
Server: server,
|
|
Login: login,
|
|
Insecure: &c.usrCfg.Insecure,
|
|
AuthType: &c.value.authType,
|
|
Debug: viper.GetBool("debug"),
|
|
MTLSConfig: jira.MTLSConfig{
|
|
CaCert: c.value.mtls.caCert,
|
|
ClientCert: c.value.mtls.clientCert,
|
|
ClientKey: c.value.mtls.clientKey,
|
|
},
|
|
})
|
|
ret, err := c.jiraClient.Me()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c.value.authType == jira.AuthTypeBearer {
|
|
login = ret.Login
|
|
}
|
|
|
|
c.value.server = server
|
|
c.value.login = login
|
|
c.value.timezone = ret.Timezone
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureServerMeta(server, login string) error {
|
|
s := cmdutil.Info("Fetching server details...")
|
|
defer s.Stop()
|
|
|
|
server = strings.TrimRight(server, "/")
|
|
|
|
c.jiraClient = api.Client(jira.Config{
|
|
Server: server,
|
|
Login: login,
|
|
Insecure: &c.usrCfg.Insecure,
|
|
AuthType: &c.value.authType,
|
|
Debug: viper.GetBool("debug"),
|
|
MTLSConfig: jira.MTLSConfig{
|
|
CaCert: c.value.mtls.caCert,
|
|
ClientCert: c.value.mtls.clientCert,
|
|
ClientKey: c.value.mtls.clientKey,
|
|
},
|
|
})
|
|
info, err := c.jiraClient.ServerInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(info.VersionNumbers) == 3 {
|
|
c.value.version.major = info.VersionNumbers[0]
|
|
c.value.version.minor = info.VersionNumbers[1]
|
|
c.value.version.patch = info.VersionNumbers[2]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func (c *JiraCLIConfigGenerator) configureProjectAndBoardDetails() error {
|
|
project := c.usrCfg.Project
|
|
board := c.usrCfg.Board
|
|
|
|
if err := c.getProjectSuggestions(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.usrCfg.Project == "" {
|
|
projectPrompt := survey.Select{
|
|
Message: "Default project:",
|
|
Help: "This is your project key that you want to access by default when using the cli.",
|
|
Options: c.projectSuggestions,
|
|
}
|
|
if err := survey.AskOne(&projectPrompt, &project, survey.WithValidator(survey.Required)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c.value.project = c.projectsMap[strings.ToLower(project)]
|
|
|
|
if c.value.project == nil {
|
|
return fmt.Errorf("project not found\n Please check the project key and try again")
|
|
}
|
|
|
|
if err := c.getBoardSuggestions(project); err != nil {
|
|
return err
|
|
}
|
|
defaultBoardSuggestions := c.boardSuggestions
|
|
|
|
if c.usrCfg.Board == "" {
|
|
for {
|
|
boardPrompt := &survey.Question{
|
|
Name: "",
|
|
Prompt: &survey.Select{
|
|
Message: "Default board:",
|
|
Help: "This is your default project board that you want to access by default when using the cli.",
|
|
Options: c.boardSuggestions,
|
|
},
|
|
Validate: func(val interface{}) error {
|
|
errInvalidSelection := fmt.Errorf("invalid selection")
|
|
|
|
ans, ok := val.(core.OptionAnswer)
|
|
if !ok {
|
|
return errInvalidSelection
|
|
}
|
|
if ans.Value == "" || ans.Value == lineBreak {
|
|
return errInvalidSelection
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
if err := survey.Ask([]*survey.Question{boardPrompt}, &board, survey.WithValidator(survey.Required)); err != nil {
|
|
return err
|
|
}
|
|
if board != optionBack && board != optionSearch {
|
|
break
|
|
}
|
|
if board == optionBack {
|
|
c.boardSuggestions = defaultBoardSuggestions
|
|
}
|
|
if board == optionSearch {
|
|
kw, err := c.getSearchKeyword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.searchAndAssignBoard(project, kw); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
c.value.board = c.boardsMap[strings.ToLower(board)]
|
|
|
|
if c.value.board == nil && !strings.EqualFold(board, optionNone) {
|
|
var suggest string
|
|
if len(defaultBoardSuggestions) > 2 {
|
|
suggest = strings.Join(defaultBoardSuggestions[2:], ", ")
|
|
} else {
|
|
suggest = strings.Join(defaultBoardSuggestions, ", ")
|
|
}
|
|
return fmt.Errorf(
|
|
"board not found\n Boards available for the project '%s' are '%s'",
|
|
c.value.project.Key,
|
|
suggest,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (*JiraCLIConfigGenerator) getSearchKeyword() (string, error) {
|
|
var ans string
|
|
|
|
qs := &survey.Question{
|
|
Name: "board",
|
|
Prompt: &survey.Input{
|
|
Message: "Search board:",
|
|
Help: "Type board name to search",
|
|
},
|
|
Validate: func(val interface{}) error {
|
|
errInvalidKeyword := fmt.Errorf("enter atleast 3 characters to search")
|
|
|
|
str, ok := val.(string)
|
|
if !ok {
|
|
return errInvalidKeyword
|
|
}
|
|
if len(str) < 3 {
|
|
return errInvalidKeyword
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
if err := survey.Ask([]*survey.Question{qs}, &ans); err != nil {
|
|
return "", err
|
|
}
|
|
return ans, nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) searchAndAssignBoard(project, keyword string) error {
|
|
resp, err := c.jiraClient.BoardSearch(project, keyword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.boardSuggestions = []string{}
|
|
for _, board := range resp.Boards {
|
|
c.boardsMap[strings.ToLower(board.Name)] = board
|
|
c.boardSuggestions = append(c.boardSuggestions, board.Name)
|
|
}
|
|
c.boardSuggestions = append(c.boardSuggestions, lineBreak, optionSearch, optionBack)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureMetadata() error {
|
|
var err error
|
|
|
|
//nolint:gomnd
|
|
isV9Compatible := c.value.version.major >= 9 || (c.value.version.major == 8 && c.value.version.minor > 4)
|
|
if c.value.installation == jira.InstallationTypeLocal && isV9Compatible {
|
|
err = c.configureIssueTypesForJiraServerV9()
|
|
} else {
|
|
err = c.configureIssueTypes()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.configureFields()
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureIssueTypes() error {
|
|
s := cmdutil.Info("Configuring metadata. Please wait...")
|
|
defer s.Stop()
|
|
|
|
meta, err := c.jiraClient.GetCreateMeta(&jira.CreateMetaRequest{
|
|
Projects: c.value.project.Key,
|
|
Expand: "projects.issuetypes.fields",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(meta.Projects) == 0 || len(meta.Projects[0].IssueTypes) == 0 {
|
|
return ErrUnexpectedResponseFormat
|
|
}
|
|
|
|
issueTypes := make([]*jira.IssueType, 0, len(meta.Projects[0].IssueTypes))
|
|
|
|
for _, it := range meta.Projects[0].IssueTypes {
|
|
issueType := jira.IssueType{
|
|
ID: it.ID,
|
|
Name: it.Name,
|
|
Handle: it.Handle,
|
|
Subtask: it.Subtask,
|
|
}
|
|
issueTypes = append(issueTypes, &issueType)
|
|
}
|
|
|
|
c.value.issueTypes = issueTypes
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureIssueTypesForJiraServerV9() error {
|
|
s := cmdutil.Info("Configuring metadata. Please wait...")
|
|
defer s.Stop()
|
|
|
|
meta, err := c.jiraClient.GetCreateMetaForJiraServerV9(&jira.CreateMetaRequest{
|
|
Projects: c.value.project.Key,
|
|
Expand: "projects.issuetypes.fields",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(meta.Values) == 0 {
|
|
return ErrUnexpectedResponseFormat
|
|
}
|
|
|
|
issueTypes := make([]*jira.IssueType, 0, len(meta.Values))
|
|
|
|
for _, it := range meta.Values {
|
|
issueType := jira.IssueType{
|
|
ID: it.ID,
|
|
Name: it.Name,
|
|
Subtask: it.Subtask,
|
|
}
|
|
issueTypes = append(issueTypes, &issueType)
|
|
}
|
|
|
|
c.value.issueTypes = issueTypes
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) configureFields() error {
|
|
customFields := make([]*issueTypeFieldConf, 0)
|
|
|
|
fields, err := c.jiraClient.GetField()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var epic jira.Epic
|
|
|
|
for _, field := range fields {
|
|
if !field.Custom {
|
|
continue
|
|
}
|
|
if field.Name == jira.EpicFieldName {
|
|
epic.Name = field.ID
|
|
continue
|
|
}
|
|
if field.Name == jira.EpicFieldLink {
|
|
epic.Link = field.ID
|
|
continue
|
|
}
|
|
customFields = append(customFields, &issueTypeFieldConf{
|
|
Name: field.Name,
|
|
Key: field.ID,
|
|
Schema: struct {
|
|
DataType string `yaml:"datatype"`
|
|
Items string `yaml:"items,omitempty"`
|
|
}{
|
|
DataType: field.Schema.DataType,
|
|
Items: field.Schema.Items,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.value.epic = &epic
|
|
c.value.customFields = customFields
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) write(path string) (string, error) {
|
|
name := func() string {
|
|
ext := filepath.Ext(path)
|
|
if ext == "" {
|
|
return path
|
|
}
|
|
return strings.TrimSuffix(filepath.Base(path), ext)
|
|
}
|
|
|
|
config := viper.New()
|
|
config.AddConfigPath(filepath.Dir(path))
|
|
config.SetConfigName(name())
|
|
config.SetConfigType(FileType)
|
|
|
|
if c.usrCfg.Insecure {
|
|
config.Set("insecure", c.usrCfg.Insecure)
|
|
}
|
|
|
|
config.Set("installation", c.value.installation)
|
|
config.Set("server", c.value.server)
|
|
config.Set("login", c.value.login)
|
|
config.Set("project", c.value.project)
|
|
config.Set("epic", c.value.epic)
|
|
config.Set("issue.types", c.value.issueTypes)
|
|
config.Set("issue.fields.custom", c.value.customFields)
|
|
config.Set("auth_type", c.value.authType.String())
|
|
config.Set("timezone", c.value.timezone)
|
|
|
|
// MTLS.
|
|
if c.value.mtls.caCert != "" {
|
|
config.Set("mtls.ca_cert", c.value.mtls.caCert)
|
|
config.Set("mtls.client_cert", c.value.mtls.clientCert)
|
|
config.Set("mtls.client_key", c.value.mtls.clientKey)
|
|
}
|
|
|
|
// Jira version.
|
|
if c.value.version.major > 0 {
|
|
config.Set("version.major", c.value.version.major)
|
|
config.Set("version.minor", c.value.version.minor)
|
|
config.Set("version.patch", c.value.version.patch)
|
|
}
|
|
|
|
if c.value.board != nil {
|
|
config.Set("board", c.value.board)
|
|
} else {
|
|
config.Set("board", "")
|
|
}
|
|
|
|
if err := config.WriteConfig(); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) getProjectSuggestions() error {
|
|
s := cmdutil.Info("Fetching projects...")
|
|
defer s.Stop()
|
|
|
|
projects, err := c.jiraClient.Project()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, project := range projects {
|
|
c.projectsMap[strings.ToLower(project.Key)] = &projectConf{
|
|
Key: project.Key,
|
|
Type: project.Type,
|
|
}
|
|
c.projectSuggestions = append(c.projectSuggestions, project.Key)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *JiraCLIConfigGenerator) getBoardSuggestions(project string) error {
|
|
s := cmdutil.Info(fmt.Sprintf("Fetching boards for project '%s'...", project))
|
|
defer s.Stop()
|
|
|
|
resp, err := c.jiraClient.Boards(project, "")
|
|
if err != nil {
|
|
if c.value.installation == jira.InstallationTypeCloud {
|
|
return err
|
|
}
|
|
// We don't care about the error in the local instance since board API may not exist if agile-addon is not installed.
|
|
// The only option available for board selection, in this case, is "None" if not passed directly from the flag.
|
|
c.boardSuggestions = append(c.boardSuggestions, optionNone)
|
|
return nil
|
|
}
|
|
c.boardSuggestions = append(c.boardSuggestions, optionSearch, lineBreak)
|
|
for _, board := range resp.Boards {
|
|
c.boardsMap[strings.ToLower(board.Name)] = board
|
|
c.boardSuggestions = append(c.boardSuggestions, board.Name)
|
|
}
|
|
c.boardSuggestions = append(c.boardSuggestions, optionNone)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Exists checks if the file exist.
|
|
func Exists(file string) bool {
|
|
if file == "" {
|
|
return false
|
|
}
|
|
if _, err := os.Stat(file); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func shallOverwrite() bool {
|
|
var ans bool
|
|
|
|
prompt := &survey.Confirm{
|
|
Message: "Config already exist. Do you want to overwrite?",
|
|
}
|
|
if err := survey.AskOne(prompt, &ans); err != nil {
|
|
return false
|
|
}
|
|
|
|
return ans
|
|
}
|
|
|
|
func create(file string) error {
|
|
const perm = 0o700
|
|
|
|
path := filepath.Dir(file)
|
|
if !Exists(path) {
|
|
if err := os.MkdirAll(path, perm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if Exists(file) {
|
|
if err := os.Rename(file, file+".bkp"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
f, err := os.Create(file)
|
|
defer func() { _ = f.Close() }()
|
|
|
|
return err
|
|
}
|