|  | @@ -1,242 +0,0 @@
 | 
	
		
			
			| 1 |  | -package main
 | 
	
		
			
			| 2 |  | -
 | 
	
		
			
			| 3 |  | -import (
 | 
	
		
			
			| 4 |  | -	"io/ioutil"
 | 
	
		
			
			| 5 |  | -	"strings"
 | 
	
		
			
			| 6 |  | -
 | 
	
		
			
			| 7 |  | -	"config"
 | 
	
		
			
			| 8 |  | -	"git"
 | 
	
		
			
			| 9 |  | -	"grafana/helpers"
 | 
	
		
			
			| 10 |  | -	puller "puller"
 | 
	
		
			
			| 11 |  | -
 | 
	
		
			
			| 12 |  | -	"github.com/sirupsen/logrus"
 | 
	
		
			
			| 13 |  | -	"gopkg.in/go-playground/webhooks.v3"
 | 
	
		
			
			| 14 |  | -	"gopkg.in/go-playground/webhooks.v3/gitlab"
 | 
	
		
			
			| 15 |  | -)
 | 
	
		
			
			| 16 |  | -
 | 
	
		
			
			| 17 |  | -// SetupWebhook creates and exposes a GitLab webhook using a given configuration.
 | 
	
		
			
			| 18 |  | -// Returns an error if the webhook couldn't be set up.
 | 
	
		
			
			| 19 |  | -func SetupWebhook(cfg *config.Config) error {
 | 
	
		
			
			| 20 |  | -	hook := gitlab.New(&gitlab.Config{
 | 
	
		
			
			| 21 |  | -		Secret: cfg.Webhook.Secret,
 | 
	
		
			
			| 22 |  | -	})
 | 
	
		
			
			| 23 |  | -	hook.RegisterEvents(HandlePush, gitlab.PushEvents)
 | 
	
		
			
			| 24 |  | -
 | 
	
		
			
			| 25 |  | -	return webhooks.Run(
 | 
	
		
			
			| 26 |  | -		hook,
 | 
	
		
			
			| 27 |  | -		cfg.Webhook.Interface+":"+cfg.Webhook.Port,
 | 
	
		
			
			| 28 |  | -		cfg.Webhook.Path,
 | 
	
		
			
			| 29 |  | -	)
 | 
	
		
			
			| 30 |  | -}
 | 
	
		
			
			| 31 |  | -
 | 
	
		
			
			| 32 |  | -// HandlePush is called each time a push event is sent by GitLab on the webhook.
 | 
	
		
			
			| 33 |  | -func HandlePush(payload interface{}, header webhooks.Header) {
 | 
	
		
			
			| 34 |  | -	var err error
 | 
	
		
			
			| 35 |  | -
 | 
	
		
			
			| 36 |  | -	// Process the payload using the right structure
 | 
	
		
			
			| 37 |  | -	pl := payload.(gitlab.PushEventPayload)
 | 
	
		
			
			| 38 |  | -
 | 
	
		
			
			| 39 |  | -	// Only push changes made on master to Grafana
 | 
	
		
			
			| 40 |  | -	if pl.Ref != "refs/heads/master" {
 | 
	
		
			
			| 41 |  | -		return
 | 
	
		
			
			| 42 |  | -	}
 | 
	
		
			
			| 43 |  | -
 | 
	
		
			
			| 44 |  | -	// Clone or pull the repository
 | 
	
		
			
			| 45 |  | -	if _, err = git.Sync(cfg.Git); err != nil {
 | 
	
		
			
			| 46 |  | -		logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 47 |  | -			"error":      err,
 | 
	
		
			
			| 48 |  | -			"repo":       cfg.Git.User + "@" + cfg.Git.URL,
 | 
	
		
			
			| 49 |  | -			"clone_path": cfg.Git.ClonePath,
 | 
	
		
			
			| 50 |  | -		}).Error("Failed to synchronise the Git repository with the remote")
 | 
	
		
			
			| 51 |  | -
 | 
	
		
			
			| 52 |  | -		return
 | 
	
		
			
			| 53 |  | -	}
 | 
	
		
			
			| 54 |  | -
 | 
	
		
			
			| 55 |  | -	// Files to push and their contents are stored in a map before being pushed
 | 
	
		
			
			| 56 |  | -	// to the Grafana API. We don't push them in the loop iterating over commits
 | 
	
		
			
			| 57 |  | -	// because, in the case a file is successively updated by two commits pushed
 | 
	
		
			
			| 58 |  | -	// at the same time, it would push the same file several time, which isn't
 | 
	
		
			
			| 59 |  | -	// an optimised behaviour.
 | 
	
		
			
			| 60 |  | -	filesToPush := make(map[string][]byte)
 | 
	
		
			
			| 61 |  | -
 | 
	
		
			
			| 62 |  | -	// Iterate over the commits descriptions from the payload
 | 
	
		
			
			| 63 |  | -	for _, commit := range pl.Commits {
 | 
	
		
			
			| 64 |  | -		// We don't want to process commits made by the puller
 | 
	
		
			
			| 65 |  | -		if commit.Author.Email == cfg.Git.CommitsAuthor.Email {
 | 
	
		
			
			| 66 |  | -			logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 67 |  | -				"hash":          commit.ID,
 | 
	
		
			
			| 68 |  | -				"author_email":  commit.Author.Email,
 | 
	
		
			
			| 69 |  | -				"manager_email": cfg.Git.CommitsAuthor.Email,
 | 
	
		
			
			| 70 |  | -			}).Info("Commit was made by the manager, skipping")
 | 
	
		
			
			| 71 |  | -
 | 
	
		
			
			| 72 |  | -			continue
 | 
	
		
			
			| 73 |  | -		}
 | 
	
		
			
			| 74 |  | -
 | 
	
		
			
			| 75 |  | -		// Set all added files to be pushed, except the ones describing a
 | 
	
		
			
			| 76 |  | -		// dashboard which name starts with a the prefix specified in the
 | 
	
		
			
			| 77 |  | -		// configuration file.
 | 
	
		
			
			| 78 |  | -		for _, addedFile := range commit.Added {
 | 
	
		
			
			| 79 |  | -			if err = prepareForPush(addedFile, &filesToPush); err != nil {
 | 
	
		
			
			| 80 |  | -				logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 81 |  | -					"error":    err,
 | 
	
		
			
			| 82 |  | -					"filename": addedFile,
 | 
	
		
			
			| 83 |  | -				}).Error("Failed to prepare file for push")
 | 
	
		
			
			| 84 |  | -
 | 
	
		
			
			| 85 |  | -				continue
 | 
	
		
			
			| 86 |  | -			}
 | 
	
		
			
			| 87 |  | -		}
 | 
	
		
			
			| 88 |  | -
 | 
	
		
			
			| 89 |  | -		// Set all modified files to be pushed, except the ones describing a
 | 
	
		
			
			| 90 |  | -		// dashboard which name starts with a the prefix specified in the
 | 
	
		
			
			| 91 |  | -		// configuration file.
 | 
	
		
			
			| 92 |  | -		for _, modifiedFile := range commit.Modified {
 | 
	
		
			
			| 93 |  | -			if err = prepareForPush(modifiedFile, &filesToPush); err != nil {
 | 
	
		
			
			| 94 |  | -				logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 95 |  | -					"error":    err,
 | 
	
		
			
			| 96 |  | -					"filename": modifiedFile,
 | 
	
		
			
			| 97 |  | -				}).Error("Failed to prepare file for push")
 | 
	
		
			
			| 98 |  | -
 | 
	
		
			
			| 99 |  | -				continue
 | 
	
		
			
			| 100 |  | -			}
 | 
	
		
			
			| 101 |  | -		}
 | 
	
		
			
			| 102 |  | -
 | 
	
		
			
			| 103 |  | -		// If a file describing a dashboard gets removed from the Git repository,
 | 
	
		
			
			| 104 |  | -		// delete the corresponding dashboard on Grafana, but only if the user
 | 
	
		
			
			| 105 |  | -		// mentionned they want to do so with the correct command line flag.
 | 
	
		
			
			| 106 |  | -		if *deleteRemoved {
 | 
	
		
			
			| 107 |  | -			for _, removedFile := range commit.Removed {
 | 
	
		
			
			| 108 |  | -				if err = deleteDashboard(removedFile); err != nil {
 | 
	
		
			
			| 109 |  | -					logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 110 |  | -						"error":    err,
 | 
	
		
			
			| 111 |  | -						"filename": removedFile,
 | 
	
		
			
			| 112 |  | -					}).Error("Failed to delete the dashboard")
 | 
	
		
			
			| 113 |  | -				}
 | 
	
		
			
			| 114 |  | -
 | 
	
		
			
			| 115 |  | -				continue
 | 
	
		
			
			| 116 |  | -			}
 | 
	
		
			
			| 117 |  | -		}
 | 
	
		
			
			| 118 |  | -	}
 | 
	
		
			
			| 119 |  | -
 | 
	
		
			
			| 120 |  | -	// Push all files to the Grafana API
 | 
	
		
			
			| 121 |  | -	for fileToPush, fileContent := range filesToPush {
 | 
	
		
			
			| 122 |  | -		if err = grafanaClient.CreateOrUpdateDashboard(fileContent); err != nil {
 | 
	
		
			
			| 123 |  | -			logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 124 |  | -				"error":    err,
 | 
	
		
			
			| 125 |  | -				"filename": fileToPush,
 | 
	
		
			
			| 126 |  | -			}).Error("Failed to push the file to Grafana")
 | 
	
		
			
			| 127 |  | -
 | 
	
		
			
			| 128 |  | -			continue
 | 
	
		
			
			| 129 |  | -		}
 | 
	
		
			
			| 130 |  | -	}
 | 
	
		
			
			| 131 |  | -
 | 
	
		
			
			| 132 |  | -	// Grafana will auto-update the version number after we pushed the new
 | 
	
		
			
			| 133 |  | -	// dashboards, so we use the puller mechanic to pull the updated numbers and
 | 
	
		
			
			| 134 |  | -	// commit them in the git repo.
 | 
	
		
			
			| 135 |  | -	if err = puller.PullGrafanaAndCommit(grafanaClient, cfg); err != nil {
 | 
	
		
			
			| 136 |  | -		logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 137 |  | -			"error":      err,
 | 
	
		
			
			| 138 |  | -			"repo":       cfg.Git.User + "@" + cfg.Git.URL,
 | 
	
		
			
			| 139 |  | -			"clone_path": cfg.Git.ClonePath,
 | 
	
		
			
			| 140 |  | -		}).Error("Call to puller returned an error")
 | 
	
		
			
			| 141 |  | -	}
 | 
	
		
			
			| 142 |  | -}
 | 
	
		
			
			| 143 |  | -
 | 
	
		
			
			| 144 |  | -// prepareForPush reads the file containing the JSON representation of a
 | 
	
		
			
			| 145 |  | -// dashboard, checks if the dashboard is set to be ignored, and if not appends
 | 
	
		
			
			| 146 |  | -// its content to a map, which will be later iterated over to push the contents
 | 
	
		
			
			| 147 |  | -// it contains to the Grafana API.
 | 
	
		
			
			| 148 |  | -// Returns an error if there was an issue reading the file or checking if the
 | 
	
		
			
			| 149 |  | -// dashboard it represents is to be ignored.
 | 
	
		
			
			| 150 |  | -func prepareForPush(
 | 
	
		
			
			| 151 |  | -	filename string, filesToPush *map[string][]byte,
 | 
	
		
			
			| 152 |  | -) (err error) {
 | 
	
		
			
			| 153 |  | -	// Don't set versions.json to be pushed
 | 
	
		
			
			| 154 |  | -	if strings.HasSuffix(filename, "versions.json") {
 | 
	
		
			
			| 155 |  | -		return
 | 
	
		
			
			| 156 |  | -	}
 | 
	
		
			
			| 157 |  | -
 | 
	
		
			
			| 158 |  | -	// Read the file's content
 | 
	
		
			
			| 159 |  | -	fileContent, err := ioutil.ReadFile(filename)
 | 
	
		
			
			| 160 |  | -	if err != nil {
 | 
	
		
			
			| 161 |  | -		return
 | 
	
		
			
			| 162 |  | -	}
 | 
	
		
			
			| 163 |  | -
 | 
	
		
			
			| 164 |  | -	// Check if dashboard is ignored
 | 
	
		
			
			| 165 |  | -	ignored, err := isIgnored(fileContent)
 | 
	
		
			
			| 166 |  | -	if err != nil {
 | 
	
		
			
			| 167 |  | -		return
 | 
	
		
			
			| 168 |  | -	}
 | 
	
		
			
			| 169 |  | -
 | 
	
		
			
			| 170 |  | -	// Append to the list of contents to push to Grafana
 | 
	
		
			
			| 171 |  | -	if !ignored {
 | 
	
		
			
			| 172 |  | -		logrus.WithFields(logrus.Fields{
 | 
	
		
			
			| 173 |  | -			"filename": filename,
 | 
	
		
			
			| 174 |  | -		}).Info("Preparing file to be pushed to Grafana")
 | 
	
		
			
			| 175 |  | -
 | 
	
		
			
			| 176 |  | -		(*filesToPush)[filename] = fileContent
 | 
	
		
			
			| 177 |  | -	}
 | 
	
		
			
			| 178 |  | -
 | 
	
		
			
			| 179 |  | -	return
 | 
	
		
			
			| 180 |  | -}
 | 
	
		
			
			| 181 |  | -
 | 
	
		
			
			| 182 |  | -// deleteDashboard reads the dashboard described in a given file and, if the file
 | 
	
		
			
			| 183 |  | -// isn't set to be ignored, delete the corresponding dashboard from Grafana.
 | 
	
		
			
			| 184 |  | -// Returns an error if there was an issue reading the file's content, checking
 | 
	
		
			
			| 185 |  | -// if the dashboard is to be ignored, computing its slug or deleting it from
 | 
	
		
			
			| 186 |  | -// Grafana.
 | 
	
		
			
			| 187 |  | -func deleteDashboard(filename string) (err error) {
 | 
	
		
			
			| 188 |  | -	// Don't delete versions.json
 | 
	
		
			
			| 189 |  | -	if strings.HasSuffix(filename, "versions.json") {
 | 
	
		
			
			| 190 |  | -		return
 | 
	
		
			
			| 191 |  | -	}
 | 
	
		
			
			| 192 |  | -
 | 
	
		
			
			| 193 |  | -	// Read the file's content
 | 
	
		
			
			| 194 |  | -	fileContent, err := ioutil.ReadFile(filename)
 | 
	
		
			
			| 195 |  | -	if err != nil {
 | 
	
		
			
			| 196 |  | -		return
 | 
	
		
			
			| 197 |  | -	}
 | 
	
		
			
			| 198 |  | -
 | 
	
		
			
			| 199 |  | -	// Check if dashboard is ignored
 | 
	
		
			
			| 200 |  | -	ignored, err := isIgnored(fileContent)
 | 
	
		
			
			| 201 |  | -	if err != nil {
 | 
	
		
			
			| 202 |  | -		return
 | 
	
		
			
			| 203 |  | -	}
 | 
	
		
			
			| 204 |  | -
 | 
	
		
			
			| 205 |  | -	if !ignored {
 | 
	
		
			
			| 206 |  | -		// Retrieve dashboard slug because we need it in the deletion request.
 | 
	
		
			
			| 207 |  | -		var slug string
 | 
	
		
			
			| 208 |  | -		slug, err = helpers.GetDashboardSlug(fileContent)
 | 
	
		
			
			| 209 |  | -		if err != nil {
 | 
	
		
			
			| 210 |  | -			return
 | 
	
		
			
			| 211 |  | -		}
 | 
	
		
			
			| 212 |  | -
 | 
	
		
			
			| 213 |  | -		// Delete the dashboard
 | 
	
		
			
			| 214 |  | -		err = grafanaClient.DeleteDashboard(slug)
 | 
	
		
			
			| 215 |  | -	}
 | 
	
		
			
			| 216 |  | -
 | 
	
		
			
			| 217 |  | -	return
 | 
	
		
			
			| 218 |  | -}
 | 
	
		
			
			| 219 |  | -
 | 
	
		
			
			| 220 |  | -// isIgnored checks whether the file must be ignored, by checking if there's an
 | 
	
		
			
			| 221 |  | -// prefix for ignored files set in the configuration file, and if the dashboard
 | 
	
		
			
			| 222 |  | -// described in the file has a name that starts with this prefix. Returns an
 | 
	
		
			
			| 223 |  | -// error if there was an issue reading or decoding the file.
 | 
	
		
			
			| 224 |  | -func isIgnored(dashboardJSON []byte) (bool, error) {
 | 
	
		
			
			| 225 |  | -	// If there's no prefix set, no file is ignored
 | 
	
		
			
			| 226 |  | -	if len(cfg.Grafana.IgnorePrefix) == 0 {
 | 
	
		
			
			| 227 |  | -		return false, nil
 | 
	
		
			
			| 228 |  | -	}
 | 
	
		
			
			| 229 |  | -
 | 
	
		
			
			| 230 |  | -	// Parse the file's content to extract its slug
 | 
	
		
			
			| 231 |  | -	slug, err := helpers.GetDashboardSlug(dashboardJSON)
 | 
	
		
			
			| 232 |  | -	if err != nil {
 | 
	
		
			
			| 233 |  | -		return false, err
 | 
	
		
			
			| 234 |  | -	}
 | 
	
		
			
			| 235 |  | -
 | 
	
		
			
			| 236 |  | -	// Compare the slug against the prefix
 | 
	
		
			
			| 237 |  | -	if strings.HasPrefix(slug, cfg.Grafana.IgnorePrefix) {
 | 
	
		
			
			| 238 |  | -		return true, nil
 | 
	
		
			
			| 239 |  | -	}
 | 
	
		
			
			| 240 |  | -
 | 
	
		
			
			| 241 |  | -	return false, nil
 | 
	
		
			
			| 242 |  | -}
 |