package cmd import ( "fmt" "os" "strings" "git.cryptic.systems/volker.raschek/drone-email-docker/pkg/domain" "git.cryptic.systems/volker.raschek/drone-email-docker/pkg/flags" "git.cryptic.systems/volker.raschek/drone-email-docker/pkg/mail" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" ) const ( // The name of our config file, without the file extension because viper // supports many different config file languages. defaultConfigFilename = "config" defaultConfigExtension = "yaml" // The environment variable prefix of all environment variables bound to our command line flags. // For example, --number is bound to STING_NUMBER. envPrefix = "" ) func Execute(version string) error { rootCmd := &cobra.Command{ Use: "drone-email", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return initializeConfig(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { vars, err := newHTMLTemplateVarsByCommand(cmd) if err != nil { return fmt.Errorf("failed to initialize new html template vars: %w", err) } smtpSettings, err := newSMTPSettingsByCommand(cmd) if err != nil { return fmt.Errorf("failed to initialize new config vars: %w", err) } recipients, err := cmd.Flags().GetStringArray(flags.SMTP_TO_ADDRESSES) if err != nil { return fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_TO_ADDRESSES, err) } err = mail.NewPlugin(smtpSettings).Exec(cmd.Context(), recipients, vars) if err != nil { return fmt.Errorf("failed to execute mail plugin: %w", err) } _, err = fmt.Fprint(os.Stdout, "E-Mails successfully sent") if err != nil { return fmt.Errorf("failed to write message on stdout: %w", err) } return nil }, SilenceUsage: true, Version: version, } hostname, err := os.Hostname() if err != nil { return fmt.Errorf("failed to detect hostname: %w", err) } // Drone environment variables/flags // Flags which receive their values from environment variables of the drone // CI/CD. // // The names of the FLags must match the environment variables, otherwise the // environment variables will not be bound correctly to the flags. rootCmd.Flags().Int64(flags.DRONE_BUILD_CREATED, 0, "Build created") rootCmd.Flags().Int64(flags.DRONE_BUILD_FINISHED, 0, "Build finished") rootCmd.Flags().Int64(flags.DRONE_BUILD_STARTED, 0, "Build stared") rootCmd.Flags().String(flags.DRONE_BUILD_EVENT, "push", "Build event") rootCmd.Flags().String(flags.DRONE_BUILD_LINK, "", "Build link") rootCmd.Flags().Int(flags.DRONE_BUILD_NUMBER, 0, "Build number") rootCmd.Flags().String(flags.DRONE_BUILD_STATUS, "success", "Build status") rootCmd.Flags().String(flags.DRONE_COMMIT_SHA, "", "SHA sum of the commit") rootCmd.Flags().String(flags.DRONE_COMMIT_REF, "refs/heads/master", "Commit reference") rootCmd.Flags().String(flags.DRONE_COMMIT_BRANCH, "master", "Commit branch") rootCmd.Flags().String(flags.DRONE_COMMIT_LINK, "", "Link to the commit") rootCmd.Flags().String(flags.DRONE_COMMIT_MESSAGE, "", "Commit message") rootCmd.Flags().String(flags.DRONE_COMMIT_AUTHOR_NAME, "", "Name of the commit author") rootCmd.Flags().String(flags.DRONE_COMMIT_AUTHOR_EMAIL, "", "E-Mail of the commit author") rootCmd.Flags().String(flags.DRONE_COMMIT_AUTHOR_AVATAR, "", "Avatar of the commit author") rootCmd.Flags().String(flags.DRONE_DEPLOY_TO, "", "Deploy target") rootCmd.Flags().String(flags.DRONE_JOB_NUMBER, "", "Job number") rootCmd.Flags().String(flags.DRONE_JOB_STATUS, "", "Job status") rootCmd.Flags().Int(flags.DRONE_JOB_EXIT_CODE, 0, "Job exit code") rootCmd.Flags().Int(flags.DRONE_JOB_STARTED, 0, "Job started") rootCmd.Flags().Int(flags.DRONE_JOB_FINISHED, 0, "Job finished") rootCmd.Flags().String(flags.DRONE_PREV_BUILD_STATUS, "", "Previous build status") rootCmd.Flags().Int(flags.DRONE_PREV_BUILD_NUMBER, 0, "Previous build number") rootCmd.Flags().String(flags.DRONE_PREV_COMMIT_SHA, "", "Previous commit sha sum") rootCmd.Flags().Int(flags.DRONE_PULL_REQUEST, 0, "Number of pull-request") rootCmd.Flags().String(flags.DRONE_REMOTE_URL, "", "Clone URL of the repository") rootCmd.Flags().Bool(flags.DRONE_REPO_PRIVATE, true, "Repository is private") rootCmd.Flags().Bool(flags.DRONE_REPO_TRUSTED, false, "Repository is trusted") rootCmd.Flags().String(flags.DRONE_REPO_AVATAR, "", "Avatar URL of the repository") rootCmd.Flags().String(flags.DRONE_REPO_BRANCH, "master", "Branch of the repository") rootCmd.Flags().String(flags.DRONE_REPO_LINK, "", "URL to the repository") rootCmd.Flags().String(flags.DRONE_REPO_NAME, "", "Name of the repository") rootCmd.Flags().String(flags.DRONE_REPO_OWNER, "", "Name of the repository owner") rootCmd.Flags().String(flags.DRONE_REPO_SCM, "git", "Source code management provider") rootCmd.Flags().String(flags.DRONE_REPO, "", "Full name of the repository") rootCmd.Flags().String(flags.DRONE_TAG, "", "Tag") rootCmd.Flags().Bool(flags.DRONE_YAML_SIGNED, false, "YAML is signed") rootCmd.Flags().Bool(flags.DRONE_YAML_VERIFIED, false, "YAML is verified") // MAIL SETTINGS rootCmd.Flags().Bool(flags.SMTP_START_TLS, mail.DefaultSMTPStartTLS, "Use StartTLS instead of SSL") rootCmd.Flags().Bool(flags.SMTP_TLS_INSECURE_SKIP_VERIFY, mail.DefaultSMTPTLSInsecureSkipVerify, "Trust insecure TLS certificates") rootCmd.Flags().Int(flags.SMTP_PORT, mail.DefaultSMTPPort, "SMTP-Port") rootCmd.Flags().String(flags.SMTP_FROM_ADDRESS, mail.DefaultSMTPFromAddress, "SMTP-From Address") rootCmd.Flags().String(flags.SMTP_FROM_NAME, mail.DefaultSMTPFromName, "SMTP-From Name") rootCmd.Flags().String(flags.SMTP_HELO, hostname, "SMTP-HELO/EHLO") rootCmd.Flags().String(flags.SMTP_HOST, mail.DefaultSMTPHost, "SMTP-Host") rootCmd.Flags().String(flags.SMTP_PASSWORD, "", "SMTP-Password") rootCmd.Flags().String(flags.SMTP_USERNAME, "", "SMTP-User") rootCmd.Flags().StringArray(flags.SMTP_TO_ADDRESSES, []string{}, "List of recipients") rootCmd.AddCommand(completionCmd) err = rootCmd.Execute() if err != nil { return fmt.Errorf("failed to execute root cmd: %w", err) } return nil } func initializeConfig(cmd *cobra.Command) error { v := viper.New() // Set the base name of the config file, without the file extension. v.SetConfigName(defaultConfigFilename) v.SetConfigType(defaultConfigExtension) // Set as many paths as you like where viper should look for the // config file. We are only looking in the current working directory. v.AddConfigPath(".") v.AddConfigPath("$HOME/.config/drone-email/") v.AddConfigPath("/etc/drone-email/") // Attempt to read the config file, gracefully ignoring errors // caused by a config file not being found. Return an error // if we cannot parse the config file. if err := v.ReadInConfig(); err != nil { // It's okay if there isn't a config file if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return fmt.Errorf("failed to read config: %w", err) } } // Bind to environment variables // Works great for simple config names, but needs help for names // like --favorite-color which we fix in the bindFlags function v.AutomaticEnv() // Bind the current command's flags to viper bindFlags(cmd, v) return nil } // Bind each cobra flag to its associated viper configuration (config file and environment variable) func bindFlags(cmd *cobra.Command, v *viper.Viper) { cmd.Flags().VisitAll(func(f *pflag.Flag) { // Environment variables can't have dashes in them, so bind them to their equivalent // keys with underscores, e.g. --favorite-color to STING_FAVORITE_COLOR if strings.Contains(f.Name, "-") { envVarSuffix := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_")) if len(envPrefix) <= 0 { _ = v.BindEnv(f.Name, envVarSuffix) } else { _ = v.BindEnv(f.Name, fmt.Sprintf("%s_%s", envPrefix, envVarSuffix)) } } // Apply the viper config value to the flag when the flag is not set and viper has a value if !f.Changed && v.IsSet(f.Name) { val := v.Get(f.Name) _ = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) } }) } func newHTMLTemplateVarsByCommand(cmd *cobra.Command) (*mail.CIVars, error) { build, err := newBuildByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new build struct: %w", err) } commit, err := newCommitByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new commit struct: %w", err) } deployTo, err := cmd.Flags().GetString(flags.DRONE_DEPLOY_TO) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_DEPLOY_TO, err) } job, err := newJobByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new job struct: %w", err) } prev, err := newPrevByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new prev struct: %w", err) } pullRequests, err := cmd.Flags().GetInt(flags.DRONE_PULL_REQUEST) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_PULL_REQUEST, err) } remote, err := newRemoteByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new remote struct: %w", err) } repo, err := newRepoByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new repo struct: %w", err) } tag, err := cmd.Flags().GetString(flags.DRONE_TAG) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_TAG, err) } yaml, err := newYAMLByCommand(cmd) if err != nil { return nil, fmt.Errorf("failed to initialize new yaml struct: %w", err) } return &mail.CIVars{ Build: build, Commit: commit, DeployTo: deployTo, Job: job, Prev: prev, PullRequest: pullRequests, Remote: remote, Repo: repo, Tag: tag, Yaml: yaml, }, nil } func newBuildByCommand(cmd *cobra.Command) (*domain.Build, error) { buildCreated, err := cmd.Flags().GetInt64(flags.DRONE_BUILD_CREATED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_CREATED, err) } buildEvent, err := cmd.Flags().GetString(flags.DRONE_BUILD_EVENT) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_EVENT, err) } buildFinished, err := cmd.Flags().GetInt64(flags.DRONE_BUILD_FINISHED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_FINISHED, err) } buildLink, err := cmd.Flags().GetString(flags.DRONE_BUILD_LINK) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_LINK, err) } buildNumber, err := cmd.Flags().GetInt(flags.DRONE_BUILD_NUMBER) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_NUMBER, err) } buildStared, err := cmd.Flags().GetInt64(flags.DRONE_BUILD_STARTED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_STARTED, err) } buildStatus, err := cmd.Flags().GetString(flags.DRONE_BUILD_STATUS) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_STATUS, err) } build := &domain.Build{ Created: buildCreated, Event: buildEvent, Finished: buildFinished, Link: buildLink, Number: buildNumber, Started: buildStared, Status: buildStatus, } return build, nil } func newCommitByCommand(cmd *cobra.Command) (*domain.Commit, error) { authorAvatar, err := cmd.Flags().GetString(flags.DRONE_COMMIT_AUTHOR_AVATAR) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_AUTHOR_AVATAR, err) } authorEmail, err := cmd.Flags().GetString(flags.DRONE_COMMIT_AUTHOR_EMAIL) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_AUTHOR_EMAIL, err) } authorName, err := cmd.Flags().GetString(flags.DRONE_COMMIT_AUTHOR_NAME) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_AUTHOR_NAME, err) } branch, err := cmd.Flags().GetString(flags.DRONE_COMMIT_BRANCH) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_BRANCH, err) } link, err := cmd.Flags().GetString(flags.DRONE_COMMIT_LINK) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_LINK, err) } message, err := cmd.Flags().GetString(flags.DRONE_COMMIT_MESSAGE) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_MESSAGE, err) } ref, err := cmd.Flags().GetString(flags.DRONE_COMMIT_REF) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_REF, err) } sha, err := cmd.Flags().GetString(flags.DRONE_COMMIT_SHA) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_COMMIT_SHA, err) } commit := &domain.Commit{ Author: &domain.Author{ Avatar: authorAvatar, Email: authorEmail, Name: authorName, }, Branch: branch, Link: link, Message: message, Ref: ref, Sha: sha, } return commit, nil } func newJobByCommand(cmd *cobra.Command) (*domain.Job, error) { exitCode, err := cmd.Flags().GetInt(flags.DRONE_JOB_EXIT_CODE) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_JOB_EXIT_CODE, err) } finished, err := cmd.Flags().GetInt64(flags.DRONE_BUILD_FINISHED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_FINISHED, err) } started, err := cmd.Flags().GetInt64(flags.DRONE_BUILD_STARTED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_BUILD_STARTED, err) } status, err := cmd.Flags().GetString(flags.DRONE_JOB_STATUS) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_JOB_STATUS, err) } job := &domain.Job{ ExitCode: exitCode, Finished: finished, Started: started, Status: status, } return job, nil } func newPrevByCommand(cmd *cobra.Command) (*domain.Prev, error) { prevBuildNumber, err := cmd.Flags().GetInt(flags.DRONE_PREV_BUILD_NUMBER) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_PREV_BUILD_NUMBER, err) } prevBuildStatus, err := cmd.Flags().GetString(flags.DRONE_PREV_BUILD_STATUS) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_PREV_BUILD_STATUS, err) } prevCommitSha, err := cmd.Flags().GetString(flags.DRONE_PREV_COMMIT_SHA) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_PREV_COMMIT_SHA, err) } prev := &domain.Prev{ Build: &domain.PrevBuild{ Number: prevBuildNumber, Status: prevBuildStatus, }, Commit: &domain.PrevCommit{ Sha: prevCommitSha, }, } return prev, nil } func newRemoteByCommand(cmd *cobra.Command) (*domain.Remote, error) { remoteURL, err := cmd.Flags().GetString(flags.DRONE_REMOTE_URL) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REMOTE_URL, err) } remote := &domain.Remote{ URL: remoteURL, } return remote, nil } func newRepoByCommand(cmd *cobra.Command) (*domain.Repo, error) { avatar, err := cmd.Flags().GetString(flags.DRONE_REPO_AVATAR) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_AVATAR, err) } branch, err := cmd.Flags().GetString(flags.DRONE_REPO_BRANCH) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_BRANCH, err) } fullName, err := cmd.Flags().GetString(flags.DRONE_REPO_NAME) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_NAME, err) } link, err := cmd.Flags().GetString(flags.DRONE_REPO_LINK) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_LINK, err) } name, err := cmd.Flags().GetString(flags.DRONE_REPO) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO, err) } owner, err := cmd.Flags().GetString(flags.DRONE_REPO_OWNER) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_OWNER, err) } private, err := cmd.Flags().GetBool(flags.DRONE_REPO_PRIVATE) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_PRIVATE, err) } scm, err := cmd.Flags().GetString(flags.DRONE_REPO_SCM) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_SCM, err) } trusted, err := cmd.Flags().GetBool(flags.DRONE_REPO_TRUSTED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_REPO_TRUSTED, err) } remote := &domain.Repo{ Avatar: avatar, Branch: branch, FullName: fullName, Link: link, Name: name, Owner: owner, Private: private, SCM: scm, Trusted: trusted, } return remote, nil } func newYAMLByCommand(cmd *cobra.Command) (*domain.Yaml, error) { signed, err := cmd.Flags().GetBool(flags.DRONE_YAML_SIGNED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_YAML_SIGNED, err) } verified, err := cmd.Flags().GetBool(flags.DRONE_YAML_VERIFIED) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.DRONE_YAML_VERIFIED, err) } yaml := &domain.Yaml{ Signed: signed, Verified: verified, } return yaml, nil } func newSMTPSettingsByCommand(cmd *cobra.Command) (*domain.SMTPSettings, error) { smtpStartTLS, err := cmd.Flags().GetBool(flags.SMTP_START_TLS) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_START_TLS, err) } smtpFromAddress, err := cmd.Flags().GetString(flags.SMTP_FROM_ADDRESS) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_FROM_ADDRESS, err) } smtpFromName, err := cmd.Flags().GetString(flags.SMTP_FROM_NAME) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_FROM_NAME, err) } smtpHELOName, err := cmd.Flags().GetString(flags.SMTP_HELO) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_HELO, err) } smtpHost, err := cmd.Flags().GetString(flags.SMTP_HOST) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_HOST, err) } smtpPassword, err := cmd.Flags().GetString(flags.SMTP_PASSWORD) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_PASSWORD, err) } smtpPort, err := cmd.Flags().GetInt(flags.SMTP_PORT) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_PORT, err) } smtpTLSInsecureSkipVerify, err := cmd.Flags().GetBool(flags.SMTP_TLS_INSECURE_SKIP_VERIFY) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_TLS_INSECURE_SKIP_VERIFY, err) } smtpUsername, err := cmd.Flags().GetString(flags.SMTP_USERNAME) if err != nil { return nil, fmt.Errorf("failed to detect value of %s: %w", flags.SMTP_USERNAME, err) } return &domain.SMTPSettings{ FromAddress: smtpFromAddress, FromName: smtpFromName, HELOName: smtpHELOName, Host: smtpHost, Password: smtpPassword, Port: smtpPort, StartTLS: smtpStartTLS, TLSInsecureSkipVerify: smtpTLSInsecureSkipVerify, Username: smtpUsername, }, nil }