From d81d3215020a600a460dd161c8b4f0395de79c4d Mon Sep 17 00:00:00 2001 From: Mofizur Rahman Date: Wed, 27 May 2020 05:43:23 -0400 Subject: [PATCH] first pass at auto deploy cloud foundry --- internals/cron/cloudfoundry.go | 69 +++++++++++++++++++++++++ internals/cron/cron.go | 29 ++++------- internals/cron/email.go | 24 +++++++++ internals/cron/env.go | 30 +++++++++++ internals/cron/github.go | 25 +++++++++ pkg/ibmcloud/api.go | 6 --- pkg/ibmcloud/cloudant.go | 13 +++++ pkg/ibmcloud/request.go | 3 ++ pkg/ibmcloud/session.go | 13 +++++ pkg/ibmcloud/types.go | 25 +++++---- pkg/notification/github.go | 94 ++++++++++++++++++++++++++++++++++ scripts/login.sh | 6 +++ templates/message.gotmpl | 13 +++++ 13 files changed, 316 insertions(+), 34 deletions(-) create mode 100644 internals/cron/cloudfoundry.go create mode 100644 internals/cron/email.go create mode 100644 internals/cron/env.go create mode 100644 internals/cron/github.go create mode 100644 pkg/notification/github.go create mode 100755 scripts/login.sh create mode 100644 templates/message.gotmpl diff --git a/internals/cron/cloudfoundry.go b/internals/cron/cloudfoundry.go new file mode 100644 index 0000000..f616856 --- /dev/null +++ b/internals/cron/cloudfoundry.go @@ -0,0 +1,69 @@ +package cron + +import ( + "fmt" + "os" + "os/exec" +) + +func deploy(apikey, org, space, resourceGroup, region string) error { + githubUser := os.Getenv("GITHUB_USER") + githubToken := os.Getenv("GITHUB_TOKEN") + grantClusterURL := os.Getenv("GRANT_CLUSTER_REPO_URL") + + if err := cmd("ibmcloud", + "login", "--apikey", apikey, "-a", "https://cloud.ibm.com", "-r", region); err != nil { + return err + } + if err := cmd("ibmcloud", + "target", "-o", org, "-s", space, "-g", resourceGroup); err != nil { + return err + } + if err := cmd("git", + "clone", fmt.Sprintf("https://%s:%s@%s.git", githubUser, githubToken, grantClusterURL)); err != nil { + return err + } + + defer cleanup("grant-cluster") + + if err := cmd("./grant-cluster/scripts/deploy-app.sh"); err != nil { + return err + } + + return nil +} + +// Cleanup folder +func cleanup(filepaths ...string) error { + for _, filepath := range filepaths { + fi, err := os.Stat(filepath) + if err != nil { + return err + } + mode := fi.Mode() + if mode.IsDir() { + if err := os.RemoveAll(filepath); err != nil { + return err + } + } else if mode.IsRegular() { + if err := os.Remove(filepath); err != nil { + return err + } + } + } + return nil +} + +func cmd(name string, args ...string) error { + cmd := exec.Command(name, args...) + + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + + if output, err := cmd.Output(); err != nil { + return err + } else { + fmt.Printf("%s\n", output) + } + return nil +} diff --git a/internals/cron/cron.go b/internals/cron/cron.go index 581cc73..84a1286 100644 --- a/internals/cron/cron.go +++ b/internals/cron/cron.go @@ -1,9 +1,7 @@ package cron import ( - "bytes" "fmt" - "html/template" "log" "math/rand" "os" @@ -277,23 +275,18 @@ func checkCloudant() { if err := notification.Email("IBMCloud Kubernetes Admin Schedule executed", emailBody, notifyEmails...); err != nil { log.Println("error sending email") } - } - } -} -func getEmailBody(data EmailData) (string, error) { - tmpl, err := template.ParseFiles("templates/email.gohtml") - if err != nil { - log.Println("could not parse file", err) - return "", err - } - htmlTemplate := template.Must(tmpl, err) - buf := new(bytes.Buffer) + // if its a workshop deploy cloud foundry and update github issue + if !schedule.IsWorkshop { + continue + } - if err := htmlTemplate.Execute(buf, data); err != nil { - log.Println("could not parse file", err) - return "", err + setEnvs(accountID, schedule) + apikey := session.GetAPIKey(accountID) + org, space, region := "", "", "" + if err != deploy(apikey, org, space, schedule.ResourceGroupName, region); err != nil { + notification.EmailAdmin("failed deploying cloud foundry app", "

Cloud foundry app failed to deploy

") + } + } } - - return buf.String(), nil } diff --git a/internals/cron/email.go b/internals/cron/email.go new file mode 100644 index 0000000..016b7ac --- /dev/null +++ b/internals/cron/email.go @@ -0,0 +1,24 @@ +package cron + +import ( + "bytes" + "html/template" + "log" +) + +func getEmailBody(data EmailData) (string, error) { + tmpl, err := template.ParseFiles("templates/email.gohtml") + if err != nil { + log.Println("could not parse file", err) + return "", err + } + htmlTemplate := template.Must(tmpl, err) + buf := new(bytes.Buffer) + + if err := htmlTemplate.Execute(buf, data); err != nil { + log.Println("could not parse file", err) + return "", err + } + + return buf.String(), nil +} diff --git a/internals/cron/env.go b/internals/cron/env.go new file mode 100644 index 0000000..6d9bfc3 --- /dev/null +++ b/internals/cron/env.go @@ -0,0 +1,30 @@ +package cron + +import ( + "os" + + "github.com/moficodes/ibmcloud-kubernetes-admin/pkg/ibmcloud" +) + +func setEnvs(accountID string, schedule ibmcloud.Schedule) error { + + if err := os.Setenv("EVENT_NAME", schedule.EventName); err != nil { + return err + } + if err := os.Setenv("PASSWORD", schedule.Password); err != nil { + return err + } + if err := os.Setenv("RESOURCE_GROUP_NAME", schedule.ResourceGroupName); err != nil { + return err + } + if err := os.Setenv("ACCESS_GROUP_NAME", schedule.AccessGroupName); err != nil { + return err + } + if err := os.Setenv("APP_HOSTNAME", schedule.EventName); err != nil { + return err + } + if err := os.Setenv("ACCOUNT", accountID); err != nil { + return err + } + return nil +} diff --git a/internals/cron/github.go b/internals/cron/github.go new file mode 100644 index 0000000..8d6b313 --- /dev/null +++ b/internals/cron/github.go @@ -0,0 +1,25 @@ +package cron + +import ( + "encoding/base64" + "os" + + "github.com/moficodes/ibmcloud-kubernetes-admin/pkg/notification" +) + +func createComment(issue, comment string) error { + apiEndpoint := os.Getenv("GITHUB_API_ENDPOINT") + repo := os.Getenv("REPO") + owner := os.Getenv("OWNER") + githubUser := os.Getenv("GITHUB_USER") + githubToken := os.Getenv("GITHUB_TOKEN") + token := "Basic " + base64Encode(githubUser+":"+githubToken) + if err := notification.CreateComment(token, apiEndpoint, owner, repo, issue, comment); err != nil { + return err + } + return nil +} + +func base64Encode(data string) string { + return base64.StdEncoding.EncodeToString([]byte(data)) +} diff --git a/pkg/ibmcloud/api.go b/pkg/ibmcloud/api.go index d186736..9399c62 100644 --- a/pkg/ibmcloud/api.go +++ b/pkg/ibmcloud/api.go @@ -61,12 +61,6 @@ const ( const basicAuth = "Basic Yng6Yng=" -var client *http.Client - -func init() { - client = &http.Client{Timeout: time.Duration(150 * time.Second)} -} - //// useful for loagging // bodyBytes, err := ioutil.ReadAll(resp.Body) // if err != nil { diff --git a/pkg/ibmcloud/cloudant.go b/pkg/ibmcloud/cloudant.go index e00f281..b310a08 100644 --- a/pkg/ibmcloud/cloudant.go +++ b/pkg/ibmcloud/cloudant.go @@ -39,6 +39,19 @@ func setupDB(dbName string) error { return err } +func GetAPIKey(accountID string) (string, err) { + dbName := "db-" + accountID + return getAPIKey(dbName) +} + +func getAPIKey(dbName string) (string, error) { + apiKey, err := getAPIKey(dbName) + if err != nil { + return "", err + } + return apiKey.APIKey, nil +} + func CheckAPIKey(accountID string) error { dbName := "db-" + accountID return checkExistingAPIKey(dbName) diff --git a/pkg/ibmcloud/request.go b/pkg/ibmcloud/request.go index 5430ae9..cc3e180 100644 --- a/pkg/ibmcloud/request.go +++ b/pkg/ibmcloud/request.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strings" + "time" ) func handleRequest(request *http.Request, header map[string]string, query map[string]string, res interface{}) error { @@ -23,6 +24,8 @@ func handleRequest(request *http.Request, header map[string]string, query map[st request.URL.RawQuery = q.Encode() + client := &http.Client{Timeout: time.Duration(150 * time.Second)} + resp, err := client.Do(request) if err != nil { return err diff --git a/pkg/ibmcloud/session.go b/pkg/ibmcloud/session.go index 6da8fbe..b2dd729 100644 --- a/pkg/ibmcloud/session.go +++ b/pkg/ibmcloud/session.go @@ -218,6 +218,19 @@ func (s *Session) CheckAPIKey(accountID string) error { return CheckAPIKey(accountID) } +func (s *Session) GetAPIKey(accountID string) (string, error) { + if !s.IsValid() { + log.Println("Access token expired.") + token, err := upgradeToken(endpoints.TokenEndpoint, s.Token.RefreshToken, accountID) + if err != nil { + return err + } + log.Println("Token Refreshed.") + s.Token = token + } + return GetAPIKey(accountID) +} + func (s *Session) UpdateAPIKey(apiKey, accountID string) error { if !s.IsValid() { log.Println("Access token expired.") diff --git a/pkg/ibmcloud/types.go b/pkg/ibmcloud/types.go index 0fd99d6..f308c76 100644 --- a/pkg/ibmcloud/types.go +++ b/pkg/ibmcloud/types.go @@ -417,16 +417,21 @@ type CreateClusterRequest struct { } type Schedule struct { - ID string `json:"_id" mapstructure:"_id"` - Rev string `json:"_rev" mapstructure:"_rev"` - CreateAt int `json:"createAt"` - DestroyAt int `json:"destroyAt"` - Status string `json:"status"` - Tags string `json:"tags"` - Count string `json:"count"` - CreateRequest CreateClusterRequest `json:"createRequest"` - Clusters []string `json:"clusters"` - NotifyEmails []string `json:"notifyEmails"` + ID string `json:"_id" mapstructure:"_id"` + Rev string `json:"_rev" mapstructure:"_rev"` + CreateAt int `json:"createAt"` + DestroyAt int `json:"destroyAt"` + Status string `json:"status"` + Tags string `json:"tags"` + Count string `json:"count"` + CreateRequest CreateClusterRequest `json:"createRequest"` + Clusters []string `json:"clusters"` + NotifyEmails []string `json:"notifyEmails"` + EventName string `json:"eventName"` + Password string `json:"password"` + ResourceGroupName string `json:"resourceGroupName"` + AccessGroupName string `json:"accessGroupName"` + IsWorkshop bool `json:"isWorkshop"` } type Vlan struct { diff --git a/pkg/notification/github.go b/pkg/notification/github.go new file mode 100644 index 0000000..4b35109 --- /dev/null +++ b/pkg/notification/github.go @@ -0,0 +1,94 @@ +package notification + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "time" +) + +const githubBaseURL = "https://api.github.ibm.com" + +func CreateComment(token, apiEndpoint, owner, repo, issue, comment string) error { + endpoint := fmt.Sprintf("%s/repos/%s/%s/issues/%s/comments", githubBaseURL, owner, repo, issue) + + header := map[string]string{ + "Authorization": token, + } + + type Comment struct { + Body string `json:"body"` + } + + c := &Comment{ + Body: comment, + } + + jsonValue, err := json.Marshal(c) + if err != nil { + return err + } + + var res interface{} + + if err := postBody(endpoint, header, nil, jsonValue, res); err != nil { + return err + } + return nil +} + +// postBody makes a post request with json body +func postBody(endpoint string, header, query map[string]string, jsonValue []byte, res interface{}) error { + request, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(jsonValue)) + if err != nil { + return err + } + + return handleRequest(request, header, query, res) +} + +func handleRequest(request *http.Request, header map[string]string, query map[string]string, res interface{}) error { + for key, value := range header { + request.Header.Add(key, value) + } + + q := request.URL.Query() + for key, value := range query { + q.Add(key, value) + } + + request.URL.RawQuery = q.Encode() + + client := &http.Client{Timeout: time.Duration(150 * time.Second)} + + resp, err := client.Do(request) + if err != nil { + return err + } + + defer resp.Body.Close() + + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + json, err := json.Marshal(resp.Body) + if err != nil { + log.Println(err) + } + return errors.New(string(json)) + } + + // b, _ := ioutil.ReadAll(resp.Body) + // log.Println(string(b)) + // This was a delete request and was successful. + // no need to try decode the body. + if resp.StatusCode == 204 { + return nil + } + + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return err + } + return nil +} diff --git a/scripts/login.sh b/scripts/login.sh new file mode 100755 index 0000000..d98f764 --- /dev/null +++ b/scripts/login.sh @@ -0,0 +1,6 @@ +#!/bin/bash +ibmcloud login --apikey $APIKEY -a https://cloud.ibm.com -r us-south + +ibmcloud target -o advowork@us.ibm.com -s dev + +ibmcloud account list \ No newline at end of file diff --git a/templates/message.gotmpl b/templates/message.gotmpl new file mode 100644 index 0000000..b7d2cb4 --- /dev/null +++ b/templates/message.gotmpl @@ -0,0 +1,13 @@ +"URL: https://{{ .EventName }}.mybluemix.net + +Key: `{{ .Password }}` + +Region: {{ .CreateClusterRequest.ClusterRequest.DataCenter }} +Clusters: {{ .Count }} +Workers: {{ .CreateClusterRequest.ClusterRequest.WorkerNum }} x {{ .CreateClusterRequest.ClusterRequest.MachineType }} +K8s Version: {{ .CreateClusterRequest.ClusterRequest.MasterVersion }} + +## Note +- Please be sure to click: `Prefill Cache` button on the URL before your lab +- Don't forget you have your https://{{ .EventName }}.mybluemix.net/admin to see the status of the clusters too +- If you need a `cloudshell` you can use https://shell.cloud.ibm.com/. It should be attached to the IBMid, that the student will have created for your workshop. If you have issues with it though please ask questions in #cloudshell-users, we do not control it. \ No newline at end of file