Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/go_modules/go-6a3dbd348c
Browse files Browse the repository at this point in the history
  • Loading branch information
dhollinger authored Nov 23, 2024
2 parents 6b13b0f + f5bf5aa commit 5f412a2
Show file tree
Hide file tree
Showing 20 changed files with 154 additions and 80 deletions.
31 changes: 13 additions & 18 deletions api/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (
"github.com/voxpupuli/webhook-go/lib/queue"
)

// Module Controller
// ModuleController handles module deployment.
type ModuleController struct{}

// DeployModule takes int the current Gin context and parses the request
// data into a variable then executes the r10k module deploy either through
// a direct local execution of the r10k deploy module command
// DeployModule handles the deployment of a Puppet module via r10k.
// It parses the incoming webhook data, constructs the r10k command,
// and either queues the deployment or executes it immediately.
func (m ModuleController) DeployModule(c *gin.Context) {
var data parsers.Data
var h helpers.Helper
Expand All @@ -35,6 +35,7 @@ func (m ModuleController) DeployModule(c *gin.Context) {
// Parse the data from the request and error if parsing fails
err := data.ParseData(c)
if err != nil {
// Respond with error if parsing fails, notify ChatOps if enabled.
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error Parsing Webhook", "error": err})
c.Abort()
if conf.ChatOps.Enabled {
Expand All @@ -43,6 +44,7 @@ func (m ModuleController) DeployModule(c *gin.Context) {
return
}

// Handle optional branch parameter, fallback to default branch if not provided.
useBranch := c.Query("branch_only")
if useBranch != "" {
branch := ""
Expand All @@ -55,12 +57,13 @@ func (m ModuleController) DeployModule(c *gin.Context) {
cmd = append(cmd, branch)
}

// Validate module name with optional override from query parameters.
module := data.ModuleName
overrideModule := c.Query("module_name")
// Restrictions to Puppet module names are: 1) begin with lowercase letter, 2) contain lowercase, digits or underscores
if overrideModule != "" {
match, _ := regexp.MatchString("^[a-z][a-z0-9_]*$", overrideModule)
if !match {
// Invalid module name, respond with error and notify ChatOps.
c.JSON(http.StatusInternalServerError, gin.H{"message": "Invalid module name"})
c.Abort()
err = fmt.Errorf("invalid module name: module name does not match the expected pattern; got: %s, pattern: ^[a-z][a-z0-9_]*$", overrideModule)
Expand All @@ -72,26 +75,16 @@ func (m ModuleController) DeployModule(c *gin.Context) {
module = overrideModule
}

// Append module name and r10k configuration to the cmd string slice
// Append module name and r10k configuration to the command string slice.
cmd = append(cmd, module)
cmd = append(cmd, fmt.Sprintf("--config=%s", h.GetR10kConfig()))

// Set additional optional r10k flags if they are set
// Set additional optional r10k flags if they are enabled.
if conf.R10k.Verbose {
cmd = append(cmd, "-v")
}

// Pass the command to the execute function and act on the result and any error
// that is returned
//
// On an error this will:
// * Log the error and command
// * Respond with an HTTP 500 error and return the command result in JSON format
// * Abort the request
// * Notify ChatOps service if enabled
//
// On success this will:
// * Respond with an HTTP 202 and the result in JSON format
// Execute or queue the command based on server configuration.
var res interface{}
if conf.Server.Queue.Enabled {
res, err = queue.AddToQueue("module", data.ModuleName, cmd)
Expand All @@ -103,6 +96,7 @@ func (m ModuleController) DeployModule(c *gin.Context) {
}
}

// Handle error response, notify ChatOps if enabled.
if err != nil {
c.JSON(http.StatusInternalServerError, res)
c.Abort()
Expand All @@ -112,6 +106,7 @@ func (m ModuleController) DeployModule(c *gin.Context) {
return
}

// On success, respond with HTTP 202 and notify ChatOps if enabled.
c.JSON(http.StatusAccepted, res)
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusAccepted, data.ModuleName, res)
Expand Down
15 changes: 11 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import (
"github.com/voxpupuli/webhook-go/config"
)

// cfgFile is the path to the configuration file, given by the user as a flag
var cfgFile string

// version is the current version of the application
var version = "0.0.0"

// rootCmd is the root command for the application
// It is used to set up the application, and is the entry point for the Cobra CLI
var rootCmd = &cobra.Command{
Use: "webhook-go",
Version: version,
Expand All @@ -21,23 +25,26 @@ var rootCmd = &cobra.Command{
API endpoint.`,
}

// Execute is the main entry point for the application, called from main.go, and is used to execute the root command
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

// init is called when the package loads, and is used to set up the root command, and the configuration file flag
func init() {
cobra.OnInitialize(initConfig)
cobra.OnInitialize(initConfig) // tells Cobra to call the initConfig function before executing any command.

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./webhook.yml)")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./webhook.yml)") // adds a flag to the root command that allows the user to specify a configuration file
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
config.Init(&cfgFile)
config.Init(&cfgFile) // Expecting a path to a configuration file
} else {
config.Init(nil)
config.Init(nil) // No path given, use defaults
}
}
7 changes: 3 additions & 4 deletions cmd/server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd

Expand All @@ -9,17 +8,16 @@ import (
"github.com/voxpupuli/webhook-go/server"
)

// serverCmd represents the server command
// serverCmd starts the Webhook-go server, allowing it to process webhook requests.
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start the Webhook-go server",
Long: ``,
Run: startServer,
}

// init adds serverCmd to the root command.
func init() {
rootCmd.AddCommand(serverCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
Expand All @@ -31,6 +29,7 @@ func init() {
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// startServer initializes and starts the server.
func startServer(cmd *cobra.Command, args []string) {
server.Init()
}
15 changes: 11 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

var config Config

// Config is a struct that holds the configuration for the application
type Config struct {
Server struct {
Protected bool `mapstructure:"protected"`
Expand Down Expand Up @@ -47,10 +48,12 @@ type Config struct {
} `mapstructure:"r10k"`
}

// Init reads in the configuration file and populates the Config struct
func Init(path *string) {
var err error
v := viper.New()
v := viper.New() // creates a new Viper instance

// If a path is given, use it, otherwise, use the default
if path != nil {
v.SetConfigFile(*path)
} else {
Expand All @@ -61,19 +64,20 @@ func Init(path *string) {
v.AddConfigPath("../config/")
v.AddConfigPath("config/")
}
err = v.ReadInConfig()
err = v.ReadInConfig() // reads the configuration file
if err != nil {
log.Fatalf("error on parsing config file: %v", err)
}

v = setDefaults(v)
v = setDefaults(v) // sets the default values for the configuration

err = v.Unmarshal(&config)
err = v.Unmarshal(&config) // converts the configuration into the Config struct
if err != nil {
log.Fatalf("Unable to read config file: %v", err)
}
}

// Provides defualt values in case of config file doesn't define some fields
func setDefaults(v *viper.Viper) *viper.Viper {
v.SetDefault("server.port", 4000)
v.SetDefault("server.protected", false)
Expand All @@ -94,13 +98,16 @@ func setDefaults(v *viper.Viper) *viper.Viper {
return v
}

// This utility function adjusts relative paths.
// If a path doesn't start with / (indicating it’s not an absolute path), it prepends the basedir to make it a proper path.
func relativePath(basedir string, path *string) {
p := *path
if len(p) > 0 && p[0] != '/' {
*path = filepath.Join(basedir, p)
}
}

// This function simply returns the currently loaded configuration
func GetConfig() Config {
return config
}
24 changes: 16 additions & 8 deletions lib/chatops/chatops.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@ import (
"strconv"
)

// ChatOps defines the configuration for interacting with various chat services.
type ChatOps struct {
Service string
Channel string
User string
AuthToken string
ServerURI *string
TestMode bool
TestURL *string
Service string // Chat service (e.g., "slack", "rocketchat", "teams").
Channel string // Target channel or room.
User string // User initiating the action.
AuthToken string // Authentication token for the chat service.
ServerURI *string // Optional server URI for self-hosted services.
TestMode bool // Indicates if the operation is in test mode.
TestURL *string // URL for testing purposes, if applicable.
}

// ChatOpsResponse captures the response details from a chat service after a message is posted.
type ChatOpsResponse struct {
Timestamp string
Channel string
}

// ChatAttachment represents the structure of a message attachment in chat services like Slack.
type ChatAttachment struct {
AuthorName string
Title string
Text string
Color string
Color string // Color to indicate status (e.g., success, failure).
}

// PostMessage sends a formatted message to the configured chat service based on the HTTP status code
// and target environment. It returns a ChatOpsResponse or an error if posting fails.
// Supports Slack, Rocket.Chat, and Microsoft Teams.
func (c *ChatOps) PostMessage(code int, target string, output interface{}) (*ChatOpsResponse, error) {
var resp ChatOpsResponse

Expand Down Expand Up @@ -56,6 +62,8 @@ func (c *ChatOps) PostMessage(code int, target string, output interface{}) (*Cha
return &resp, nil
}

// formatMessage generates a ChatAttachment based on the HTTP status code and target environment.
// The message is used to notify the result of a Puppet environment deployment.
func (c *ChatOps) formatMessage(code int, target string) ChatAttachment {
var message ChatAttachment

Expand Down
11 changes: 10 additions & 1 deletion lib/chatops/rocketchat.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,42 @@ import (
"github.com/pandatix/gocket-chat/api/chat"
)

// rocketChat posts a message to a Rocket.Chat channel using the provided HTTP status code and target environment.
// It returns a PostMessageResponse or an error if the operation fails.
// ServerURI must be provided as part of the ChatOps configuration.
func (c *ChatOps) rocketChat(code int, target string) (*chat.PostMessageResponse, error) {
// Ensure ServerURI is set before proceeding.
if c.ServerURI == nil {
return nil, fmt.Errorf("A ServerURI must be specified to use RocketChat")
}

client := &http.Client{}

// Initialize RocketChat client with the provided ServerURI, AuthToken, and User credentials.
rc, err := gochat.NewRocketClient(client, *c.ServerURI, c.AuthToken, c.User)
if err != nil {
return nil, err
}

var attachments []chat.Attachement
// Format the message based on the HTTP status code and target environment.
msg := c.formatMessage(code, target)

// Prepare attachments for the message.
var attachments []chat.Attachement
attachments = append(attachments, chat.Attachement{
AuthorName: msg.AuthorName,
Title: msg.Title,
Color: msg.Color,
Text: msg.Text,
})

// Set the parameters for posting the message to the specified channel.
pmp := chat.PostMessageParams{
Channel: c.Channel,
Attachements: &attachments,
}

// Post the message to RocketChat.
res, err := chat.PostMessage(rc, pmp)
if err != nil {
return nil, err
Expand Down
2 changes: 2 additions & 0 deletions lib/chatops/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"github.com/slack-go/slack"
)

// slack posts a message to a Slack channel based on the HTTP status code and target.
// Returns the channel, timestamp, or an error if the operation fails.
func (c *ChatOps) slack(code int, target string) (*string, *string, error) {
var sapi *slack.Client
if c.TestMode {
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/voxpupuli/webhook-go/lib/parsers"
)

// GetBranch returns the branch name from the parsed data. If the branch was deleted, it returns the defaultBranch.
func (h *Helper) GetBranch(data parsers.Data, defaultBranch string) string {
if data.Deleted {
return defaultBranch
Expand Down
2 changes: 2 additions & 0 deletions lib/helpers/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
)

// GetEnvironment constructs and returns an environment name by combining the prefix and branch.
// If either is empty, it normalizes the branch name. Allows optional uppercase transformation.
func (h *Helper) GetEnvironment(branch, prefix string, allowUppercase bool) string {
if prefix == "" || branch == "" {
return h.Normalize(allowUppercase, branch)
Expand Down
2 changes: 2 additions & 0 deletions lib/helpers/r10k-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import "github.com/voxpupuli/webhook-go/config"

const ConfigFile = "/etc/puppetlabs/r10k/r10k.yaml"

// GetR10kConfig retrieves the R10k configuration file path.
// If no custom path is set in the configuration, it returns the default path.
func (h *Helper) GetR10kConfig() string {
conf := config.GetConfig().R10k
confPath := conf.ConfigPath
Expand Down
5 changes: 5 additions & 0 deletions lib/parsers/azure-devops.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/mcdafydd/go-azuredevops/azuredevops"
)

// parseAzureDevops processes an Azure DevOps webhook, extracting event details such as branch, module name, and repository info.
// It handles the PushEvent type and marks the data as completed and successful upon successful parsing.
func (d *Data) parseAzureDevops(c *gin.Context) error {
payload, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
Expand Down Expand Up @@ -42,6 +44,7 @@ func (d *Data) parseAzureDevops(c *gin.Context) error {
return nil
}

// parseRawResource unmarshals the raw payload of a Git push event from Azure DevOps.
func (d *Data) parseRawResource(e *azuredevops.Event) (payload *azuredevops.GitPush, err error) {
payload = &azuredevops.GitPush{}

Expand All @@ -54,10 +57,12 @@ func (d *Data) parseRawResource(e *azuredevops.Event) (payload *azuredevops.GitP
return payload, nil
}

// azureDevopsDeleted checks if the push event represents a branch deletion in Azure DevOps.
func (d *Data) azureDevopsDeleted(e *azuredevops.GitPush) bool {
return *e.RefUpdates[0].NewObjectID == "0000000000000000000000000000000000000000"
}

// parseBranch extracts the branch name from the push event, removing the ref prefix.
func (d *Data) parseBranch(e *azuredevops.GitPush) string {
return strings.TrimPrefix(*e.RefUpdates[0].Name, prefix)
}
Loading

0 comments on commit 5f412a2

Please sign in to comment.