diff --git a/acceptance/examples/embedded_rego_config.yaml b/acceptance/examples/embedded_rego_config.yaml new file mode 100644 index 000000000..ee4f0a283 --- /dev/null +++ b/acceptance/examples/embedded_rego_config.yaml @@ -0,0 +1,4 @@ +--- +sources: + - policy: + - "git::https://${GITHOST}/git/embedded-rego-policy.git" diff --git a/acceptance/examples/embedded_rego_test.rego b/acceptance/examples/embedded_rego_test.rego new file mode 100644 index 000000000..837fec405 --- /dev/null +++ b/acceptance/examples/embedded_rego_test.rego @@ -0,0 +1,16 @@ +package main + +import data.ec_lib + +# METADATA +# custom: +# short_name: embedded_test +deny contains result if { + # Always deny + true + + # We're expecting this to be defined in the "embedded" rego + msg := ec_lib.hello_world("testy mctest") + + result := {"code": "main.embedded_test", "msg": msg} +} diff --git a/features/__snapshots__/embedded_rego.snap b/features/__snapshots__/embedded_rego.snap new file mode 100755 index 000000000..083191521 --- /dev/null +++ b/features/__snapshots__/embedded_rego.snap @@ -0,0 +1,20 @@ + +[policy using embedded rego functions:stdout - 1] +Success: false +Result: FAILURE +Violations: 1, Warnings: 0, Successes: 0 +Input File: pipeline_definition.json + +Results: +✕ [Violation] main.embedded_test + FilePath: pipeline_definition.json + Reason: Hello, testy mctest! (from embedded rego) + +For more information about policy issues, see the policy documentation: https://conforma.dev/docs/policy/ + +--- + +[policy using embedded rego functions:stderr - 1] +Error: success criteria not met + +--- diff --git a/features/embedded_rego.feature b/features/embedded_rego.feature new file mode 100644 index 000000000..3dd3bbe22 --- /dev/null +++ b/features/embedded_rego.feature @@ -0,0 +1,22 @@ +Feature: embedded rego functionality + The ec command should be able to use embedded rego functions in policy evaluation + + Background: + # Todo: We can use file paths so we don't really need git to test this + Given stub git daemon running + + Scenario: policy using embedded rego functions + Given a git repository named "embedded-rego-config" with + | policy.yaml | examples/embedded_rego_config.yaml | + Given a git repository named "embedded-rego-policy" with + | main.rego | examples/embedded_rego_test.rego | + + # The rego in embedded_rego_test ignores the input + Given a pipeline definition file named "pipeline_definition.json" containing + """ + {} + """ + + When ec command is run with "validate input --file pipeline_definition.json --policy git::https://${GITHOST}/git/embedded-rego-config.git --show-successes" + Then the exit status should be 1 + Then the output should match the snapshot diff --git a/internal/embedded/rego/ec_lib/hello_world.rego b/internal/embedded/rego/ec_lib/hello_world.rego new file mode 100644 index 000000000..1189e83f1 --- /dev/null +++ b/internal/embedded/rego/ec_lib/hello_world.rego @@ -0,0 +1,4 @@ +package ec_lib + +# Simple POC to demonstrate the idea of embedding rego in the cli +hello_world(name) := sprintf("Hello, %s! (from embedded rego)", [name]) diff --git a/internal/embedded/rego/embed.go b/internal/embedded/rego/embed.go new file mode 100644 index 000000000..20c0c0acb --- /dev/null +++ b/internal/embedded/rego/embed.go @@ -0,0 +1,26 @@ +// Copyright The Conforma Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package rego + +import "embed" + +// EmbeddedRego contains rego files embedded in the CLI binary. +// These files are available without needing to fetch them from external +// sources, solving the shared dependency problem for common rego utilities. +// +//go:embed ec_lib/*.rego +var EmbeddedRego embed.FS diff --git a/internal/embedded/rego/embed_test.go b/internal/embedded/rego/embed_test.go new file mode 100644 index 000000000..349535f30 --- /dev/null +++ b/internal/embedded/rego/embed_test.go @@ -0,0 +1,38 @@ +// Copyright The Conforma Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unit + +package rego + +import ( + "io/fs" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHelloWorldRegoContent(t *testing.T) { + // Test that the hello_world.rego file can be read and has expected content + content, err := fs.ReadFile(EmbeddedRego, "ec_lib/hello_world.rego") + require.NoError(t, err, "Should be able to read hello_world.rego") + + contentStr := string(content) + assert.Contains(t, contentStr, "package ec_lib") + assert.Contains(t, contentStr, "hello_world(name)") + assert.Contains(t, contentStr, "from embedded rego") +} diff --git a/internal/evaluator/conftest_evaluator.go b/internal/evaluator/conftest_evaluator.go index f2e434ae6..190429ce7 100644 --- a/internal/evaluator/conftest_evaluator.go +++ b/internal/evaluator/conftest_evaluator.go @@ -352,6 +352,12 @@ func NewConftestEvaluatorWithNamespaceAndFilterType( c.policyDir = filepath.Join(c.workDir, "policy") c.dataDir = filepath.Join(c.workDir, "data") + // Write embedded rego files to policy directory + if err := utils.WriteEmbeddedRego(ctx, fs, c.policyDir); err != nil { + log.Warnf("Failed to write embedded rego files: %v", err) + // Continue without embedded files - graceful degradation + } + if err := c.createDataDirectory(ctx); err != nil { return nil, err } diff --git a/internal/utils/embedded_rego_test.go b/internal/utils/embedded_rego_test.go new file mode 100644 index 000000000..884198ff1 --- /dev/null +++ b/internal/utils/embedded_rego_test.go @@ -0,0 +1,156 @@ +// Copyright The Conforma Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unit + +package utils + +import ( + "context" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteEmbeddedRego(t *testing.T) { + // Test that embedded rego files are written correctly to filesystem + ctx := context.Background() + fs := afero.NewMemMapFs() + + // Create a temporary policy directory + policyDir := "/tmp/policy" + err := fs.MkdirAll(policyDir, 0o755) + require.NoError(t, err) + + // Call WriteEmbeddedRego + err = WriteEmbeddedRego(ctx, fs, policyDir) + require.NoError(t, err, "WriteEmbeddedRego should succeed") + + // Verify embedded directory was created + embeddedDir := filepath.Join(policyDir, "embedded") + exists, err := afero.DirExists(fs, embeddedDir) + require.NoError(t, err) + assert.True(t, exists, "embedded directory should be created") + + // Verify hello_world.rego file was written + helloWorldPath := filepath.Join(embeddedDir, "ec_lib", "hello_world.rego") + exists, err = afero.Exists(fs, helloWorldPath) + require.NoError(t, err) + assert.True(t, exists, "hello_world.rego should be written") + + // Verify file content is correct + content, err := afero.ReadFile(fs, helloWorldPath) + require.NoError(t, err) + + contentStr := string(content) + assert.Contains(t, contentStr, "package ec_lib") + assert.Contains(t, contentStr, "hello_world(name)") + assert.Contains(t, contentStr, "from embedded rego") +} + +func TestWriteEmbeddedRegoMaintainsDirectoryStructure(t *testing.T) { + // Test that directory structure from embedded files is maintained + ctx := context.Background() + fs := afero.NewMemMapFs() + + policyDir := "/tmp/policy" + err := fs.MkdirAll(policyDir, 0o755) + require.NoError(t, err) + + err = WriteEmbeddedRego(ctx, fs, policyDir) + require.NoError(t, err) + + // Verify the full directory structure: policy/embedded/ec_lib/hello_world.rego + expectedPath := filepath.Join(policyDir, "embedded", "ec_lib", "hello_world.rego") + exists, err := afero.Exists(fs, expectedPath) + require.NoError(t, err) + assert.True(t, exists, "Full directory structure should be maintained") + + // Verify ec_lib directory exists + ecLibDir := filepath.Join(policyDir, "embedded", "ec_lib") + exists, err = afero.DirExists(fs, ecLibDir) + require.NoError(t, err) + assert.True(t, exists, "ec_lib subdirectory should be created") +} + +func TestWriteEmbeddedRegoFilePermissions(t *testing.T) { + // Test that files are written with correct permissions + ctx := context.Background() + fs := afero.NewMemMapFs() + + policyDir := "/tmp/policy" + err := fs.MkdirAll(policyDir, 0o755) + require.NoError(t, err) + + err = WriteEmbeddedRego(ctx, fs, policyDir) + require.NoError(t, err) + + // Check file permissions + helloWorldPath := filepath.Join(policyDir, "embedded", "ec_lib", "hello_world.rego") + info, err := fs.Stat(helloWorldPath) + require.NoError(t, err) + + // Should be readable and writable by owner, readable by group and others + assert.Equal(t, "-rw-r--r--", info.Mode().String()) +} + +func TestWriteEmbeddedRegoNonExistentPolicyDir(t *testing.T) { + // Test behavior when policy directory doesn't exist + ctx := context.Background() + fs := afero.NewMemMapFs() + + // Don't create the policy directory + policyDir := "/tmp/nonexistent/policy" + + // WriteEmbeddedRego should create necessary directories + err := WriteEmbeddedRego(ctx, fs, policyDir) + require.NoError(t, err, "Should create necessary directories") + + // Verify the embedded directory was created + embeddedDir := filepath.Join(policyDir, "embedded") + exists, err := afero.DirExists(fs, embeddedDir) + require.NoError(t, err) + assert.True(t, exists, "Should create embedded directory and parents") +} + +func TestWriteEmbeddedRegoIdempotent(t *testing.T) { + // Test that calling WriteEmbeddedRego multiple times is safe + ctx := context.Background() + fs := afero.NewMemMapFs() + + policyDir := "/tmp/policy" + err := fs.MkdirAll(policyDir, 0o755) + require.NoError(t, err) + + // Call WriteEmbeddedRego twice + err = WriteEmbeddedRego(ctx, fs, policyDir) + require.NoError(t, err, "First call should succeed") + + err = WriteEmbeddedRego(ctx, fs, policyDir) + require.NoError(t, err, "Second call should succeed (idempotent)") + + // Verify file still exists and has correct content + helloWorldPath := filepath.Join(policyDir, "embedded", "ec_lib", "hello_world.rego") + content, err := afero.ReadFile(fs, helloWorldPath) + require.NoError(t, err) + + contentStr := string(content) + assert.Contains(t, contentStr, "package ec_lib") + assert.Contains(t, contentStr, "hello_world(name)") +} diff --git a/internal/utils/helpers.go b/internal/utils/helpers.go index 082c6dc00..191f05502 100644 --- a/internal/utils/helpers.go +++ b/internal/utils/helpers.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "io/fs" "os" "path/filepath" "strings" @@ -28,6 +29,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/afero" "sigs.k8s.io/yaml" + + embeddedRego "github.com/conforma/cli/internal/embedded/rego" ) // CreateWorkDir creates the working directory in tmp and some subdirectories @@ -162,3 +165,55 @@ func anyEnvSet(varNames []string) bool { } return false } + +// WriteEmbeddedRego writes embedded rego files to the specified policy directory. +// This follows the same pattern as external policy sources, where each source +// gets its own subdirectory. Embedded rego files are placed in an "embedded" +// subdirectory to match the uniqueDestination pattern used for external sources. +func WriteEmbeddedRego(ctx context.Context, afs afero.Fs, policyDir string) error { + // Create the embedded subdirectory (follows pattern where each source gets its own subdir) + embeddedDir := filepath.Join(policyDir, "embedded") + if err := afs.MkdirAll(embeddedDir, 0o755); err != nil { + return fmt.Errorf("failed to create embedded rego directory: %w", err) + } + + // Walk through all embedded rego files and write them to the filesystem + return fs.WalkDir(embeddedRego.EmbeddedRego, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("failed to walk embedded rego file %s: %w", path, err) + } + + // Skip directories + if d.IsDir() { + return nil + } + + // Only process .rego files + if !strings.HasSuffix(path, ".rego") { + return nil + } + + // Read the embedded file content + content, err := fs.ReadFile(embeddedRego.EmbeddedRego, path) + if err != nil { + return fmt.Errorf("failed to read embedded rego file %s: %w", path, err) + } + + // Write to the target location, maintaining directory structure + targetPath := filepath.Join(embeddedDir, path) + targetDir := filepath.Dir(targetPath) + + // Ensure target directory exists + if err := afs.MkdirAll(targetDir, 0o755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", targetDir, err) + } + + // Write the file + if err := afero.WriteFile(afs, targetPath, content, 0o644); err != nil { + return fmt.Errorf("failed to write embedded rego file to %s: %w", targetPath, err) + } + + log.Debugf("Wrote embedded rego file: %s", targetPath) + return nil + }) +}