Skip to content

Commit

Permalink
feat: [ASSMT-62]: support for Spinnaker pipeline migration command (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielGz authored Feb 13, 2024
1 parent cc25d64 commit 4814059
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 0 deletions.
56 changes: 56 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package main

import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -109,3 +112,56 @@ func AuthHeaderKey(auth string) string {
}
return "x-api-key"
}

func GetWithAuth(host string, query string, authMethod string, base64Auth string, certPath string, keyPath string) (body []byte, err error) {
baseURL := "https://" + host + "/api/v1/" + query

var client *http.Client

req, err := http.NewRequest("GET", baseURL, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}

// Configure client based on authentication method
if authMethod == authBasic {
// Encode credentials to base64 for the Authorization header
client = &http.Client{}
// Add the Authorization header to the request
req.Header.Add("Authorization", "Basic "+base64Auth)
} else if authMethod == authx509 {
// Load client certificate
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
fmt.Println("Error loading certificate:", err)
return nil, err
}

// Create a HTTPS client and supply the created CA pool and certificate
config := &tls.Config{
Certificates: []tls.Certificate{cert},
// In a real application, you should adjust the TLS settings according to your security requirements.
}
client = &http.Client{Transport: &http.Transport{TLSClientConfig: config}}
} else {
fmt.Println("Unsupported authentication method")
return nil, fmt.Errorf("unsupported authentication method %s", authMethod)
}

resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return nil, err
}
defer resp.Body.Close()

// Read and print the response body
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return nil, err
}
fmt.Println(string(body))
return body, nil
}
68 changes: 68 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ var migrationReq = struct {
TargetGatewayUrl string `survey:"targetGatewayUrl"`
Force bool `survey:"force"`
Flags string `survey:"flags"`
Platform string `survey:"platform"`
SpinnakerHost string `survey:"spinnaker-host"`
SpinnakerAPIKey string `survey:"spinnaker-api-key"`
AppName string `survey:"app-name"`
PipelineName string `survey:"pipeline-name"`
Cert string `survey:"cert"`
Key string `survey:"key"`
Auth64 string `survey:"auth64"`
}{}

func getReqBody(entityType EntityType, filter Filter) RequestBody {
Expand Down Expand Up @@ -125,6 +133,25 @@ func logMigrationDetails() {
}).Info("Migration details")
}

func logSpinnakerMigrationDetails(authMethod string) {
// Manually format the log message with line breaks
logMessage := fmt.Sprintf("\nMigration details:\n"+
" Platform: %s\n"+
" Spinnaker Host: %s\n"+
" App name: %s\n"+
" Pipeline Name: %s\n"+
" Authentication method: %s",
migrationReq.Platform,
migrationReq.SpinnakerHost,
migrationReq.AppName,
migrationReq.PipelineName,
authMethod,
)

// Log the formatted message
log.Infof(logMessage)
}

func cliWrapper(fn cliFnWrapper, ctx *cli.Context) error {
if len(migrationReq.LogLevel) > 0 {
level, err := log.ParseLevel(migrationReq.LogLevel)
Expand Down Expand Up @@ -299,6 +326,22 @@ func main() {
Usage: "provide a list of flags for custom logic",
Destination: &migrationReq.Flags,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "platform",
Usage: "Specifies the platform that serves as the source for migration to the next-generation harness. Supported values: harness-legacy (default), spinnaker.",
Destination: &migrationReq.Platform,
DefaultText: legacy,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "spinnaker-host",
Usage: "Specifies URL to the Spinnaker Gate service. Required when --platform is spinnaker.",
Destination: &migrationReq.SpinnakerHost,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "spinnaker-api-key",
Usage: "Specifies URL to the Spinnaker Gate service. Required when --platform is spinnaker.",
Destination: &migrationReq.SpinnakerAPIKey,
}),
}
app := &cli.App{
Name: "harness-upgrade",
Expand Down Expand Up @@ -519,6 +562,31 @@ func main() {
Usage: "`NAMES` of the next gen pipeline",
Destination: &migrationReq.Names,
},
&cli.StringFlag{
Name: "app-name",
Usage: "Specifies Spinnaker Application from which pipelines to be migrated.",
Destination: &migrationReq.AppName,
},
&cli.StringFlag{
Name: "pipeline-name",
Usage: "Specifies Spinnaker Pipeline which to be migrated.",
Destination: &migrationReq.PipelineName,
},
&cli.StringFlag{
Name: "cert",
Usage: "Cert file location in case Spinnaker uses x509 auth",
Destination: &migrationReq.Cert,
},
&cli.StringFlag{
Name: "key",
Usage: "Optional. key file location in case Spinnaker uses x509 auth",
Destination: &migrationReq.Key,
},
&cli.StringFlag{
Name: "auth64",
Usage: "Base64 <username>:<password> in case Spinnaker uses basic auth.",
Destination: &migrationReq.Auth64,
},
},
Subcommands: []*cli.Command{
{
Expand Down
107 changes: 107 additions & 0 deletions pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ import (
"strconv"
)

const spinnaker string = "spinnaker"
const legacy string = "harness-legacy"
const authBasic string = "basic"
const authx509 string = "x509"

func migratePipelines(*cli.Context) error {
promptConfirm := PromptDefaultInputs()
if migrationReq.Platform == spinnaker {
return migrateSpinnakerPipelines()
}

if len(migrationReq.AppId) == 0 {
promptConfirm = true
migrationReq.AppId = TextInput("Please provide the application ID of the app containing the pipeline -")
Expand Down Expand Up @@ -54,6 +63,74 @@ func migratePipelines(*cli.Context) error {
return nil
}

func migrateSpinnakerPipelines() error {
authMethod := authBasic
if len(migrationReq.Cert) > 0 {
authMethod = authx509
}

if len(migrationReq.SpinnakerHost) == 0 {
migrationReq.SpinnakerHost = TextInput("Please provide spinnaker host")
}
if len(migrationReq.AppName) == 0 {
migrationReq.AppName = TextInput("Please provide the Spinnaker application name")
}

if !migrationReq.All {
migrationReq.PipelineName = TextInput("Please provide the Spinnaker pipeline name")
}

logSpinnakerMigrationDetails(authMethod)
confirm := ConfirmInput("Do you want to proceed with pipeline migration?")
if !confirm {
log.Fatal("Aborting...")
}
var jsonBody []byte
var pipelines []map[string]interface{}
var err error
if len(migrationReq.PipelineName) > 0 {
jsonBody, err = getSinglePipeline(authMethod, migrationReq.PipelineName)
} else {
jsonBody, err = getAllPipelines(authMethod)
}
if err != nil {
return err
}
pipelines, err = normalizeJsonArray(jsonBody)
payload := map[string][]map[string]interface{}{"pipelines": pipelines}
_, err = createSpinnakerPipelines(payload)
return err
}

// / normalizeJsonArray returns an array of 1 element if body is an object, otherwise returns the existing array
func normalizeJsonArray(body []byte) ([]map[string]interface{}, error) {
var temp interface{}
err := json.Unmarshal(body, &temp)
if err != nil {
return nil, err
}

var normalizedData []map[string]interface{}

switch v := temp.(type) {
case map[string]interface{}:
// If the data is a single object, wrap it in a slice
normalizedData = append(normalizedData, v)
case []interface{}:
// If the data is an array, convert each element to a map[string]interface{} and append to the slice
for _, item := range v {
if mapItem, ok := item.(map[string]interface{}); ok {
normalizedData = append(normalizedData, mapItem)
} else {
return nil, fmt.Errorf("array element is not a JSON object")
}
}
default:
return nil, fmt.Errorf("unexpected data type")
}
return normalizedData, nil
}

func BulkRemovePipelines(*cli.Context) error {
promptConfirm := PromptEnvDetails()
promptConfirm = PromptOrgAndProject([]string{Project}) || promptConfirm
Expand Down Expand Up @@ -155,3 +232,33 @@ func findPipelineIdByName(pipelines []PipelineDetails, name string) string {
}
return ""
}

func getAllPipelines(authMethod string) ([]byte, error) {
return GetWithAuth(migrationReq.SpinnakerHost, "applications/"+migrationReq.AppName+"/pipelineConfigs", authMethod, migrationReq.Auth64, migrationReq.Cert, migrationReq.Key)
}

func getSinglePipeline(authMethod string, name string) ([]byte, error) {
return GetWithAuth(migrationReq.SpinnakerHost, "applications/"+migrationReq.AppName+"/pipelineConfigs/"+name, authMethod, migrationReq.Auth64, migrationReq.Cert, migrationReq.Key)
}

func createSpinnakerPipelines(pipelines interface{}) (reqId string, err error) {
queryParams := map[string]string{
ProjectIdentifier: migrationReq.ProjectIdentifier,
OrgIdentifier: migrationReq.OrgIdentifier,
AccountIdentifier: migrationReq.Account,
}
url := GetUrlWithQueryParams(migrationReq.Environment, MigratorService, "spinnaker/pipelines", queryParams)
resp, err := Post(url, migrationReq.Auth, pipelines)
if err != nil || resp.Status != "SUCCESS" {
log.Fatal("Failed to create pipelines", err)
return
}
resource, err := getResource(resp.Resource)
if err != nil || len(resource.RequestId) == 0 {
log.Fatal("Failed to create the entities", err)
return
}
reqId = resource.RequestId
log.Infof("The request id is - %s", reqId)
return
}

0 comments on commit 4814059

Please sign in to comment.