github.com/twelho/conform@v0.0.0-20231016230407-c25e9238598a/internal/enforcer/enforcer.go (about)

     1  // This Source Code Form is subject to the terms of the Mozilla Public
     2  // License, v. 2.0. If a copy of the MPL was not distributed with this
     3  // file, You can obtain one at http://mozilla.org/MPL/2.0/.
     4  
     5  // Package enforcer defines policy enforcement.
     6  package enforcer
     7  
     8  import (
     9  	"fmt"
    10  	"log"
    11  	"os"
    12  	"text/tabwriter"
    13  
    14  	"github.com/mitchellh/mapstructure"
    15  	"github.com/pkg/errors"
    16  	yaml "gopkg.in/yaml.v2"
    17  
    18  	"github.com/twelho/conform/internal/policy"
    19  	"github.com/twelho/conform/internal/policy/commit"
    20  	"github.com/twelho/conform/internal/policy/license"
    21  	"github.com/twelho/conform/internal/reporter"
    22  )
    23  
    24  // Conform is a struct that conform.yaml gets decoded into.
    25  //
    26  //nolint:govet
    27  type Conform struct {
    28  	Policies []*PolicyDeclaration `yaml:"policies"`
    29  	reporter reporter.Reporter
    30  }
    31  
    32  // PolicyDeclaration allows a user to declare an arbitrary type along with a
    33  // spec that will be decoded into the appropriate concrete type.
    34  //
    35  //nolint:govet
    36  type PolicyDeclaration struct {
    37  	Type string      `yaml:"type"`
    38  	Spec interface{} `yaml:"spec"`
    39  }
    40  
    41  // policyMap defines the set of policies allowed within Conform.
    42  var policyMap = map[string]policy.Policy{
    43  	"commit":  &commit.Commit{},
    44  	"license": &license.License{},
    45  	// "version":    &version.Version{},
    46  }
    47  
    48  // New loads the conform.yaml file and unmarshals it into a Conform struct.
    49  func New(r string) (*Conform, error) {
    50  	c := &Conform{}
    51  
    52  	switch r {
    53  	case "github":
    54  		s, err := reporter.NewGitHubReporter()
    55  		if err != nil {
    56  			return nil, err
    57  		}
    58  
    59  		c.reporter = s
    60  	default:
    61  		c.reporter = &reporter.Noop{}
    62  	}
    63  
    64  	configBytes, err := os.ReadFile(".conform.yaml")
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	err = yaml.Unmarshal(configBytes, c)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	return c, nil
    75  }
    76  
    77  // Enforce enforces all policies defined in the conform.yaml file.
    78  func (c *Conform) Enforce(setters ...policy.Option) error {
    79  	opts := policy.NewDefaultOptions(setters...)
    80  
    81  	const padding = 8
    82  	w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', 0)
    83  	fmt.Fprintln(w, "POLICY\tCHECK\tSTATUS\tMESSAGE\t")
    84  
    85  	pass := true
    86  
    87  	for _, p := range c.Policies {
    88  		report, err := c.enforce(p, opts)
    89  		if err != nil {
    90  			log.Fatal(err)
    91  		}
    92  
    93  		for _, check := range report.Checks() {
    94  			if len(check.Errors()) != 0 {
    95  				for _, err := range check.Errors() {
    96  					fmt.Fprintf(w, "%s\t%s\t%s\t%v\t\n", p.Type, check.Name(), "FAILED", err)
    97  				}
    98  
    99  				if err := c.reporter.SetStatus("failure", p.Type, check.Name(), check.Message()); err != nil {
   100  					log.Printf("WARNING: report failed: %+v", err)
   101  				}
   102  
   103  				pass = false
   104  			} else {
   105  				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", p.Type, check.Name(), "PASS", check.Message())
   106  
   107  				if err := c.reporter.SetStatus("success", p.Type, check.Name(), check.Message()); err != nil {
   108  					log.Printf("WARNING: report failed: %+v", err)
   109  				}
   110  			}
   111  		}
   112  	}
   113  
   114  	w.Flush() //nolint:errcheck
   115  
   116  	if !pass {
   117  		return errors.New("1 or more policy failed")
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  func (c *Conform) enforce(declaration *PolicyDeclaration, opts *policy.Options) (*policy.Report, error) {
   124  	if _, ok := policyMap[declaration.Type]; !ok {
   125  		return nil, errors.Errorf("Policy %q is not defined", declaration.Type)
   126  	}
   127  
   128  	p := policyMap[declaration.Type]
   129  
   130  	// backwards compatibility, convert `gpg: bool` into `gpg: required: bool`
   131  	if declaration.Type == "commit" {
   132  		if spec, ok := declaration.Spec.(map[interface{}]interface{}); ok {
   133  			if gpg, ok := spec["gpg"]; ok {
   134  				if val, ok := gpg.(bool); ok {
   135  					spec["gpg"] = map[string]interface{}{
   136  						"required": val,
   137  					}
   138  				}
   139  			}
   140  		}
   141  	}
   142  
   143  	err := mapstructure.Decode(declaration.Spec, p)
   144  	if err != nil {
   145  		return nil, errors.Errorf("Internal error: %v", err)
   146  	}
   147  
   148  	return p.Compliance(opts)
   149  }