Files
atlantis/server/core/runtime/policy/conftest_client.go
Ken Kaizu 27b9897517 chore(build): rm pegomock experimental feature for smooth mock generation (#2886)
* disable pegomock --use-experimental-model-gen, cause random fail by segv

* make regen-mocks

* fix mock code which uses non experimental pegomock codes
2022-12-27 19:58:15 -06:00

258 lines
7.9 KiB
Go

package policy
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
version "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime/cache"
runtime_models "github.com/runatlantis/atlantis/server/core/runtime/models"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
const (
DefaultConftestVersionEnvKey = "DEFAULT_CONFTEST_VERSION"
conftestBinaryName = "conftest"
conftestDownloadURLPrefix = "https://github.com/open-policy-agent/conftest/releases/download/v"
conftestArch = "x86_64"
)
type Arg struct {
Param string
Option string
}
func (a Arg) build() []string {
return []string{a.Option, a.Param}
}
func NewPolicyArg(parameter string) Arg {
return Arg{
Param: parameter,
Option: "-p",
}
}
type ConftestTestCommandArgs struct {
PolicyArgs []Arg
ExtraArgs []string
InputFile string
Command string
}
func (c ConftestTestCommandArgs) build() ([]string, error) {
if len(c.PolicyArgs) == 0 {
return []string{}, errors.New("no policies specified")
}
// add the subcommand
commandArgs := []string{c.Command, "test"}
for _, a := range c.PolicyArgs {
commandArgs = append(commandArgs, a.build()...)
}
// add hardcoded options
commandArgs = append(commandArgs, c.InputFile, "--no-color")
// add extra args provided through server config
commandArgs = append(commandArgs, c.ExtraArgs...)
return commandArgs, nil
}
// SourceResolver resolves the policy set to a local fs path
//
//go:generate pegomock generate -m --package mocks -o mocks/mock_conftest_client.go SourceResolver
type SourceResolver interface {
Resolve(policySet valid.PolicySet) (string, error)
}
// LocalSourceResolver resolves a local policy set to a local fs path
type LocalSourceResolver struct {
}
func (p *LocalSourceResolver) Resolve(policySet valid.PolicySet) (string, error) {
return policySet.Path, nil
}
// SourceResolverProxy proxies to underlying source resolvers dynamically
type SourceResolverProxy struct {
localSourceResolver SourceResolver
}
func (p *SourceResolverProxy) Resolve(policySet valid.PolicySet) (string, error) {
switch source := policySet.Source; source {
case valid.LocalPolicySet:
return p.localSourceResolver.Resolve(policySet)
default:
return "", fmt.Errorf("unable to resolve policy set source %s", source)
}
}
type ConfTestVersionDownloader struct {
downloader terraform.Downloader
}
func (c ConfTestVersionDownloader) downloadConfTestVersion(v *version.Version, destPath string) (runtime_models.FilePath, error) {
versionURLPrefix := fmt.Sprintf("%s%s", conftestDownloadURLPrefix, v.Original())
// download binary in addition to checksum file
binURL := fmt.Sprintf("%s/conftest_%s_%s_%s.tar.gz", versionURLPrefix, v.Original(), cases.Title(language.English).String(runtime.GOOS), conftestArch)
checksumURL := fmt.Sprintf("%s/checksums.txt", versionURLPrefix)
// underlying implementation uses go-getter so the URL is formatted as such.
// i know i know, I'm assuming an interface implementation with my inputs.
// realistically though the interface just exists for testing so ¯\_(ツ)_/¯
fullSrcURL := fmt.Sprintf("%s?checksum=file:%s", binURL, checksumURL)
if err := c.downloader.GetAny(destPath, fullSrcURL); err != nil {
return runtime_models.LocalFilePath(""), errors.Wrapf(err, "downloading conftest version %s at %q", v.String(), fullSrcURL)
}
binPath := filepath.Join(destPath, "conftest")
return runtime_models.LocalFilePath(binPath), nil
}
// ConfTestExecutorWorkflow runs a versioned conftest binary with the args built from the project context.
// Project context defines whether conftest runs a local policy set or runs a test on a remote policy set.
type ConfTestExecutorWorkflow struct {
SourceResolver SourceResolver
VersionCache cache.ExecutionVersionCache
DefaultConftestVersion *version.Version
Exec runtime_models.Exec
}
func NewConfTestExecutorWorkflow(log logging.SimpleLogging, versionRootDir string, conftestDownloder terraform.Downloader) *ConfTestExecutorWorkflow {
downloader := ConfTestVersionDownloader{
downloader: conftestDownloder,
}
version, err := getDefaultVersion()
if err != nil {
// conftest default versions are not essential to service startup so let's not block on it.
log.Warn("failed to get default conftest version. Will attempt request scoped lazy loads %s", err.Error())
}
versionCache := cache.NewExecutionVersionLayeredLoadingCache(
conftestBinaryName,
versionRootDir,
downloader.downloadConfTestVersion,
)
return &ConfTestExecutorWorkflow{
VersionCache: versionCache,
DefaultConftestVersion: version,
SourceResolver: &SourceResolverProxy{
localSourceResolver: &LocalSourceResolver{},
},
Exec: runtime_models.LocalExec{},
}
}
func (c *ConfTestExecutorWorkflow) Run(ctx command.ProjectContext, executablePath string, envs map[string]string, workdir string, extraArgs []string) (string, error) {
policyArgs := []Arg{}
policySetNames := []string{}
ctx.Log.Debug("policy sets, %s ", ctx.PolicySets)
for _, policySet := range ctx.PolicySets.PolicySets {
path, err := c.SourceResolver.Resolve(policySet)
// Let's not fail the whole step because of a single failure. Log and fail silently
if err != nil {
ctx.Log.Err("Error resolving policyset %s. err: %s", policySet.Name, err.Error())
continue
}
policyArg := NewPolicyArg(path)
policyArgs = append(policyArgs, policyArg)
policySetNames = append(policySetNames, policySet.Name)
}
inputFile := filepath.Join(workdir, ctx.GetShowResultFileName())
args := ConftestTestCommandArgs{
PolicyArgs: policyArgs,
ExtraArgs: extraArgs,
InputFile: inputFile,
Command: executablePath,
}
serializedArgs, err := args.build()
if err != nil {
ctx.Log.Warn("No policies have been configured")
return "", nil
// TODO: enable when we can pass policies in otherwise e2e tests with policy checks fail
// return "", errors.Wrap(err, "building args")
}
initialOutput := fmt.Sprintf("Checking plan against the following policies: \n %s\n", strings.Join(policySetNames, "\n "))
cmdOutput, err := c.Exec.CombinedOutput(serializedArgs, envs, workdir)
return c.sanitizeOutput(inputFile, initialOutput+cmdOutput), err
}
func (c *ConfTestExecutorWorkflow) sanitizeOutput(inputFile string, output string) string {
return strings.Replace(output, inputFile, "<redacted plan file>", -1)
}
func (c *ConfTestExecutorWorkflow) EnsureExecutorVersion(log logging.SimpleLogging, v *version.Version) (string, error) {
// we have no information to proceed, so fallback to `conftest` command or fail hard
if c.DefaultConftestVersion == nil && v == nil {
localPath, err := c.Exec.LookPath(conftestBinaryName)
if err == nil {
log.Info("conftest version is not specified, so fallback to conftest command")
return localPath, nil
}
return "", errors.New("no conftest version configured/specified or not found conftest command")
}
var versionToRetrieve *version.Version
if v == nil {
versionToRetrieve = c.DefaultConftestVersion
} else {
versionToRetrieve = v
}
localPath, err := c.VersionCache.Get(versionToRetrieve)
if err != nil {
return "", err
}
return localPath, nil
}
func getDefaultVersion() (*version.Version, error) {
// ensure version is not default version.
// first check for the env var and if that doesn't exist use the local executable version
defaultVersion, exists := os.LookupEnv(DefaultConftestVersionEnvKey)
if !exists {
return nil, fmt.Errorf("%s not set", DefaultConftestVersionEnvKey)
}
wrappedVersion, err := version.NewVersion(defaultVersion)
if err != nil {
return nil, errors.Wrapf(err, "wrapping version %s", defaultVersion)
}
return wrappedVersion, nil
}