Skip to content

Commit

Permalink
feat: [GTM-3249]: Add run pipeline subcommand (#69)
Browse files Browse the repository at this point in the history
* Add pipeline execution respnonse handling, utility functions, and UI execution link
  • Loading branch information
nicholaslotz authored Feb 11, 2024
1 parent a997ed4 commit 8381022
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 26 deletions.
4 changes: 1 addition & 3 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"net/http"
"strconv"
"strings"

log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -98,12 +97,10 @@ func handleResp(req *http.Request) (respBodyObj ResponseBody, err error) {
log.WithFields(log.Fields{
"body": string(respBody),
}).Debug("The response body")

if err != nil {
return
}
_ = json.Unmarshal(respBody, &respBodyObj)

if resp.StatusCode != 200 {
if resp.StatusCode >= 400 || resp.StatusCode < 500 {
println(respBodyObj.Message)
Expand All @@ -125,3 +122,4 @@ func AuthHeaderKey(auth string) string {
}
return "x-api-key"
}

5 changes: 5 additions & 0 deletions defaults/constants.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package defaults

const API_VERSION = "v1"
const AWS_CROSS_ACCOUNT_ROLE_ARN = "ROLE_ARN"
const AWS_ACCESS_KEY = "AWS_ACCESS_KEY"
const AWS_SECRET_IDENTIFIER = "awssecret"
Expand All @@ -24,13 +25,16 @@ const HOST_IP_PLACEHOLDER = "HOST_IP_OR_FQDN"
const HOST_PORT_PLACEHOLDER = "PORT"
const INSTANCE_NAME_PLACEHOLDER = "INSTANCE_NAME"
const NG_BASE_URL = "/gateway/ng/api"
const ORGANIZATIONS_ENDPOINT = "orgs/"
const PROJECTS_ENDPOINT = "projects/"
const PROJECT_NAME_PLACEHOLDER = "CLOUD_PROJECT_NAME"
const REGION_NAME_PLACEHOLDER = "CLOUD_REGION_NAME"
const SSH_PRIVATE_KEY_SECRET_IDENTIFIER = "harness_sshprivatekey"
const SSH_KEY_FILE_SECRET_IDENTIFIER = "harness_sshsecretfile"
const WINRM_SECRET_IDENTIFIER = "harness_winrmpwd"
const WINRM_PASSWORD_SECRET_IDENTIFIER = "winrm_passwd"
const INFRA_ENDPOINT = "infrastructures"
const HARNESS_UX_VERSION = "ng"

// Enum for multiple platforms
const (
Expand All @@ -39,6 +43,7 @@ const (
)

const NOT_IMPLEMENTED = "Command Not_Implemented. Check back later.."
const EXECUTE_PIPELINE_ENDPOINT = "pipeline/execute"
const PIPELINES_BASE_URL = "/gateway/pipeline/api"
const PIPELINES_ENDPOINT = "pipelines"
const PIPELINES_ENDPOINT_V2 = "pipelines/v2"
Expand Down
Binary file added harnes
Binary file not shown.
27 changes: 26 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ func main() {
{
Name: "pipeline",
Aliases: []string{"pipeline"},
Usage: "Pipeline specific commands, eg: apply (create/update), delete, list",
Usage: "Pipeline specific commands, eg: apply (create/update), delete, run, list",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Expand Down Expand Up @@ -587,6 +587,31 @@ func main() {
return cliWrapper(deletePipeline, context)
},
},
{
Name: "run",
Usage: "Run a pipeline.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pipeline-id",
Usage: "identifier of pipeline to execute",
},
altsrc.NewStringFlag(&cli.StringFlag{
Name: "inputs-file",
Usage: "path to YAML file containing pipeline inputs",
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "org-id",
Usage: "provide an Organization Identifier",
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "project-id",
Usage: "provide a Project Identifier",
}),
},
Action: func(context *cli.Context) error {
return cliWrapper(runPipeline, context)
},
},
},
},
{
Expand Down
90 changes: 88 additions & 2 deletions pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,65 @@ func deletePipeline(*cli.Context) error {
return nil
}

// Delete an existing Pipeline
// List details of an existing Pipeline
func listPipeline(*cli.Context) error {
fmt.Println(defaults.NOT_IMPLEMENTED)
return nil
return nil
}

// Run an existing Pipeline
func runPipeline(c *cli.Context) error {
// Get path and query parameters from user and/or defaults
baseURL := GetBaseUrl(c, defaults.PIPELINES_BASE_URL)
orgIdentifier := c.String("org-id")
if orgIdentifier == "" {
orgIdentifier = defaults.DEFAULT_ORG
}
projectIdentifier := c.String("project-id")
if projectIdentifier == "" {
projectIdentifier = defaults.DEFAULT_PROJECT
}
pipelineIdentifier := c.String ("pipeline-id")
if pipelineIdentifier == "" {
return fmt.Errorf("Pipeline id required. See: harness pipeline run --help")
}

// Process optional inputs YAML into request body
var content, _ = ReadFromFile(c.String("inputs-file"))
requestBody := GetJsonFromYaml(content)

// Return error if pipeline doesn't exist
pipelineEndpoint := fmt.Sprintf("%s/%s", defaults.PIPELINES_ENDPOINT, pipelineIdentifier)
pipelineExists := GetEntity(baseURL, pipelineEndpoint, projectIdentifier, orgIdentifier, map[string]string{})
if !pipelineExists {
return fmt.Errorf("Could not fetch pipeline. Check pipeline id and scope.")
}

// Generate endpoint, tack on query parameters
execPipelineEndpoint := fmt.Sprintf("%s/%s", defaults.EXECUTE_PIPELINE_ENDPOINT, pipelineIdentifier)
execPipelineURL := GetUrlWithQueryParams("", baseURL, execPipelineEndpoint, map[string]string{
"accountIdentifier": CliCdRequestData.Account,
"orgIdentifier": orgIdentifier,
"projectIdentifier": projectIdentifier,
})

// Make execute pipeline POST call
var err error
var resp ResponseBody
resp, err = client.Post(execPipelineURL, CliCdRequestData.AuthToken, requestBody, defaults.CONTENT_TYPE_YAML, nil)

// Break and write to console if error
if err != nil {
return fmt.Errorf("Could not start execution of pipeline %s. Check pipeline configuration and inputs.", pipelineIdentifier)
}

// Otherwise print success message and execution link
pipelineExecLink := GetPipelineExecLink(resp, CliCdRequestData.Account, orgIdentifier, projectIdentifier, pipelineIdentifier)
println(GetColoredText("Successfully started execution of pipeline ", color.FgGreen) +
GetColoredText(pipelineIdentifier, color.FgBlue))
println(GetColoredText("Execution URL: ", color.FgGreen) +
GetColoredText(pipelineExecLink , color.FgYellow))
return nil
}

func yamlHasDockerUsername(str string) bool {
Expand All @@ -134,3 +189,34 @@ func yamlHasGithubUsername(str string) bool {
match, _ := regexp.MatchString(regexPattern, str)
return match
}

func GetPipelineExecLink(respBodyObj ResponseBody, accountId string, orgId string, projectId string, pipelineId string) string {
// Get Harness host from user or use default
var baseUrl string
if CliCdRequestData.BaseUrl == "" {
baseUrl = defaults.HARNESS_PROD_URL
} else {
baseUrl = CliCdRequestData.BaseUrl
}

// Get pipeline execution ID from custom HTTP reponse object
execData := respBodyObj.Data.(map[string]interface{})
planExecutionData := execData["planExecution"].(map[string]interface{})
execId := planExecutionData["uuid"]
// Build pipeline execution URL
// Start with individual entity segments
uxSegement := fmt.Sprintf("%s%s/", baseUrl, defaults.HARNESS_UX_VERSION)
accountSegment := fmt.Sprintf("%s/%s/%s/", "account", accountId, "home")
orgSegment := fmt.Sprintf("%s%s/", defaults.ORGANIZATIONS_ENDPOINT, orgId)
projectSegment := fmt.Sprintf("%s%s/", defaults.PROJECTS_ENDPOINT, projectId)
pipelineSegment := fmt.Sprintf("%s/%s/", defaults.PIPELINES_ENDPOINT, pipelineId)
execSegment := fmt.Sprintf("%s/%s/", "executions", execId)

// From previous segments, build user's project path and pipeline execution path
uxProjectPath := fmt.Sprintf("%s%s%s%s", uxSegement, accountSegment, orgSegment, projectSegment)
pipelineExecPath := fmt.Sprintf("%s%s", pipelineSegment, execSegment)

// Cat the ux and pipieline exec paths into complete URL
fullPipelineExecLink := fmt.Sprintf("%s%s", uxProjectPath, pipelineExecPath)
return fullPipelineExecLink
}
48 changes: 28 additions & 20 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,31 @@ func TextInput(question string) string {
}

func GetUrlWithQueryParams(environment string, service string, endpoint string, queryParams map[string]string) string {
params := ""
totalItems := len(queryParams)
currentIndex := 0
for k, v := range queryParams {
currentIndex++
if v != "" {
if currentIndex == totalItems {
params = params + k + "=" + v
} else {
params = params + k + "=" + v + "&"
}
}
}
// remove trailing & char
lastChar := params[len(params)-1]
if lastChar == '&' {
params = strings.TrimSuffix(params, string('&'))
}

return fmt.Sprintf("%s/%s?%s", service, endpoint, params)
// Outer if-else handles requests with no query params
if len(queryParams) > 0 {
params := ""
totalItems := len(queryParams)
currentIndex := 0
for k, v := range queryParams {
currentIndex++
if v != "" {
if currentIndex == totalItems {
params = params + k + "=" + v
} else {
params = params + k + "=" + v + "&"
}
}

}
// remove trailing & char
lastChar := params[len(params)-1]
if lastChar == '&' {
params = strings.TrimSuffix(params, string('&'))
}
return fmt.Sprintf("%s/%s?%s", service, endpoint, params)
} else {
return fmt.Sprintf("%s/%s", service, endpoint)
}
}

func PrintJson(v any) {
Expand Down Expand Up @@ -171,6 +176,7 @@ func GetJsonFromYaml(content string) map[string]interface{} {
}
return requestBody
}

func GetNestedValue(data map[string]interface{}, keys ...string) interface{} {
if len(keys) == 0 {
return nil
Expand All @@ -194,6 +200,7 @@ func GetNestedValue(data map[string]interface{}, keys ...string) interface{} {

return value
}

func GetUserHomePath() string {
homeDir, err := os.UserHomeDir()
if err != nil {
Expand All @@ -202,6 +209,7 @@ func GetUserHomePath() string {
}
return homeDir
}

func ValueToString(value interface{}) string {
switch v := value.(type) {
case string:
Expand Down

0 comments on commit 8381022

Please sign in to comment.