diff --git a/.gitignore b/.gitignore index db2bc2a..aea7037 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ main cover.out config.yml concom +.config diff --git a/Makefile b/Makefile index f5f1e7f..0459bbb 100644 --- a/Makefile +++ b/Makefile @@ -42,4 +42,13 @@ test: ## Run tests $(WARN) "Tests failed"; \ exit 1; \ fi ; \ - $(OK) Tests passed \ No newline at end of file + $(OK) Tests passed + + +build: ## Build the project + @mkdir -p dist + @go build -o dist/concom main.go + +run: ## Run the project + @go run main.go agent --config ./.config/config.yaml + diff --git a/cmd/agent.go b/cmd/agent.go index 6280a0a..eb0f624 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -6,9 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/compliance-framework/agent/runner/proto" - "github.com/google/uuid" - "github.com/robfig/cron/v3" "math/rand" "net/http" "os" @@ -16,11 +13,16 @@ import ( "os/signal" "path" "runtime" + "strconv" "strings" "sync" "syscall" "time" + "github.com/compliance-framework/agent/runner/proto" + "github.com/google/uuid" + "github.com/robfig/cron/v3" + "github.com/compliance-framework/agent/internal" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/api/sdk" @@ -46,11 +48,13 @@ type agentPolicy string type agentPluginConfig map[string]string type agentPlugin struct { - Schedule *string `mapstructure:"schedule,omitempty"` - Source string `mapstructure:"source"` - Policies []agentPolicy `mapstructure:"policies"` - Config agentPluginConfig `mapstructure:"config"` - Labels map[string]string `mapstructure:"labels"` + ProtocolVersion int32 `mapstructure:"protocol_version"` + Schedule *string `mapstructure:"schedule,omitempty"` + Source string `mapstructure:"source"` + Policies []agentPolicy `mapstructure:"policies"` + Config agentPluginConfig `mapstructure:"config"` + Labels map[string]string `mapstructure:"labels"` + protocolSet bool } type agentConfig struct { @@ -77,11 +81,32 @@ func (ac *agentConfig) validate() error { return fmt.Errorf("no api config specified in config") } + for name, pluginConfig := range ac.Plugins { + if pluginConfig == nil { + return fmt.Errorf("plugin %s has null configuration", name) + } + + if pluginConfig.ProtocolVersion == 0 { + if pluginConfig.protocolSet { + return fmt.Errorf("plugin %s has unsupported protocol_version=%d; supported values are %d and %d", name, pluginConfig.ProtocolVersion, DefaultProtocolVersion, RunnerV2ProtocolVersion) + } + + continue + } + + if !isSupportedProtocolVersion(pluginConfig.ProtocolVersion) { + return fmt.Errorf("plugin %s has unsupported protocol_version=%d; supported values are %d and %d", name, pluginConfig.ProtocolVersion, DefaultProtocolVersion, RunnerV2ProtocolVersion) + } + } + return nil } const AgentPluginDir = ".compliance-framework/plugins" const AgentPolicyDir = ".compliance-framework/policies" +const DefaultProtocolVersion int32 = 1 +const RunnerV2ProtocolVersion int32 = 2 +const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version" func AgentCmd() *cobra.Command { var agentCmd = &cobra.Command{ @@ -138,13 +163,89 @@ func mergeConfig(cmd *cobra.Command, fileConfig *viper.Viper) (*agentConfig, err config := &agentConfig{} err := fileConfig.Unmarshal(config) + if err != nil { return nil, err } + markExplicitPluginProtocols(fileConfig, config) + updateAllPluginProtocols(config) + return config, nil } +func markExplicitPluginProtocols(fileConfig *viper.Viper, config *agentConfig) { + rawPlugins := fileConfig.GetStringMap("plugins") + for name, rawPlugin := range rawPlugins { + pluginConfig, ok := config.Plugins[name] + if rawPlugin == nil { + if config.Plugins == nil { + config.Plugins = map[string]*agentPlugin{} + } + if !ok { + config.Plugins[name] = nil + } + continue + } + + if !ok || pluginConfig == nil { + continue + } + + pluginMap, ok := rawPlugin.(map[string]interface{}) + if !ok { + continue + } + + _, pluginConfig.protocolSet = pluginMap["protocol_version"] + } +} + +func updateAllPluginProtocols(agentConfig *agentConfig) { + for _, pluginConfig := range agentConfig.Plugins { + if pluginConfig != nil && !pluginConfig.protocolSet && pluginConfig.ProtocolVersion == 0 { + pluginConfig.ProtocolVersion = DefaultProtocolVersion + } + } +} + +func isSupportedProtocolVersion(protocolVersion int32) bool { + return protocolVersion == DefaultProtocolVersion || protocolVersion == RunnerV2ProtocolVersion +} + +func protocolVersionFromAnnotations(annotations map[string]string) (int32, bool) { + value, ok := annotations[AnnotationProtocolVersionKey] + if !ok { + return 0, false + } + + parsed, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return 0, false + } + + if parsed < 1 { + return 0, false + } + + if !isSupportedProtocolVersion(int32(parsed)) { + return 0, false + } + + return int32(parsed), true +} + +func runnerDispenseName(protocolVersion int32) (string, error) { + switch protocolVersion { + case DefaultProtocolVersion: + return "runner", nil + case RunnerV2ProtocolVersion: + return "runner-v2", nil + default: + return "", fmt.Errorf("unsupported plugin protocol_version=%d", protocolVersion) + } +} + func loadConfig(cmd *cobra.Command, v *viper.Viper) (*agentConfig, error) { err := v.ReadInConfig() if err != nil { @@ -234,16 +335,18 @@ type AgentRunner struct { mu sync.Mutex config *agentConfig - pluginLocations map[string]string - policyLocations map[string]string + pluginLocations map[string]string + policyLocations map[string]string + fetchAnnotations func(ctx context.Context, source string, option ...remote.Option) (map[string]string, error) queryBundles []*rego.Rego } func NewAgentRunner() *AgentRunner { return &AgentRunner{ - pluginLocations: map[string]string{}, - policyLocations: map[string]string{}, + pluginLocations: map[string]string{}, + policyLocations: map[string]string{}, + fetchAnnotations: internal.GetAnnotations, } } @@ -266,6 +369,8 @@ func (ar *AgentRunner) Run(ctx context.Context) error { return err } + ar.resolvePluginProtocols(ctx) + err = ar.DownloadPolicies(ctx) if err != nil { ar.logger.Error("Error downloading policies", "error", err) @@ -281,6 +386,42 @@ func (ar *AgentRunner) Run(ctx context.Context) error { return ar.runAllPlugins(ctx) } +func (ar *AgentRunner) resolvePluginProtocols(ctx context.Context) { + if ctx == nil { + ctx = context.Background() + } + + for pluginName, pluginConfig := range ar.config.Plugins { + if pluginConfig == nil || pluginConfig.protocolSet || !internal.IsOCI(pluginConfig.Source) { + continue + } + + func() { + annotationCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + annotations, err := ar.fetchAnnotations(annotationCtx, pluginConfig.Source) + if err != nil { + ar.logger.Warn("Failed to fetch plugin annotations, using configured/default protocol version", "plugin", pluginName, "source", pluginConfig.Source, "protocol_version", pluginConfig.ProtocolVersion, "error", err) + return + } + + value, ok := annotations[AnnotationProtocolVersionKey] + if !ok { + return + } + + protocolVersion, ok := protocolVersionFromAnnotations(annotations) + if !ok { + ar.logger.Warn("Ignoring unsupported plugin protocol version annotation", "plugin", pluginName, "source", pluginConfig.Source, "value", value, "protocol_version", pluginConfig.ProtocolVersion) + return + } + + pluginConfig.ProtocolVersion = protocolVersion + }() + } +} + // Should never return, either handles any error or panics. func (ar *AgentRunner) runDaemon(ctx context.Context) { sigs := make(chan os.Signal, 1) @@ -363,7 +504,7 @@ func (ar *AgentRunner) setupCron(ctx context.Context) (*cron.Cron, error) { err := ar.runPlugin(ctx, pluginName, pluginConfig) if err != nil { // TODO how will we handle these errors ? - ar.logger.Error("Error running plugin", "error", err) + ar.logger.Error("Error running plugin", "error", err, "protocol_version", pluginConfig.ProtocolVersion) } }) @@ -405,13 +546,13 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error { source := ar.pluginLocations[pluginConfig.Source] - logger.Debug("Running plugin", "source", source) + logger.Debug("Running plugin", "source", source, "protocol_version", pluginConfig.ProtocolVersion) if _, err := os.ReadFile(source); err != nil { return err } - runnerInstance, err := ar.getRunnerInstance(logger, source) + runnerInstance, err := ar.getRunnerInstance(logger, source, pluginConfig.ProtocolVersion) if err != nil { return err @@ -444,6 +585,21 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error { // Create a new results helper for the plugin to send results back to resultsHelper := runner.NewApiHelper(logger, client, labels) + if pluginConfig.ProtocolVersion > 1 { + runnerV2, ok := runnerInstance.(runner.RunnerV2) + if !ok { + return fmt.Errorf("plugin %s configured as protocol_version=%d but does not support RunnerV2", pluginName, pluginConfig.ProtocolVersion) + } + + _, err := runnerV2.Init(&proto.InitRequest{ + PolicyPaths: policyPaths, + }, resultsHelper) + + if err != nil { + return err + } + } + // TODO: Send failed results to the database? _, err = runnerInstance.Eval(&proto.EvalRequest{ PolicyPaths: policyPaths, @@ -496,13 +652,13 @@ func (ar *AgentRunner) runPlugin(ctx context.Context, name string, plugin *agent OS: runtime.GOOS, })) - fmt.Println("Running plugin", "source", plugin.Source) - fmt.Println("Running plugin", "source", pluginExecutable) - if err != nil { return err } + ar.logger.Info("Running plugin", "source", plugin.Source, "protocol_version", plugin.ProtocolVersion) + ar.logger.Info("Running plugin", "source", pluginExecutable, "protocol_version", plugin.ProtocolVersion) + logger := hclog.New(&hclog.LoggerOptions{ Name: fmt.Sprintf("runner.%s", name), Output: os.Stdout, @@ -517,13 +673,13 @@ func (ar *AgentRunner) runPlugin(ctx context.Context, name string, plugin *agent labels[k] = v } - logger.Debug("Running plugin", "source", pluginExecutable) + logger.Debug("Running plugin", "source", pluginExecutable, "protocol_version", plugin.ProtocolVersion) if _, err := os.ReadFile(pluginExecutable); err != nil { return err } - runnerInstance, err := ar.getRunnerInstance(logger, pluginExecutable) + runnerInstance, err := ar.getRunnerInstance(logger, pluginExecutable, plugin.ProtocolVersion) if err != nil { return err @@ -539,6 +695,21 @@ func (ar *AgentRunner) runPlugin(ctx context.Context, name string, plugin *agent // Create a new results helper for the plugin to send results back to resultsHelper := runner.NewApiHelper(logger, client, labels) + if plugin.ProtocolVersion > 1 { + runnerV2, ok := runnerInstance.(runner.RunnerV2) + if !ok { + return fmt.Errorf("plugin %s configured as protocol_version=%d but does not support RunnerV2", name, plugin.ProtocolVersion) + } + + _, err := runnerV2.Init(&proto.InitRequest{ + PolicyPaths: policyPaths, + }, resultsHelper) + + if err != nil { + return err + } + } + // TODO: Send failed results to the database? _, err = runnerInstance.Eval(&proto.EvalRequest{ PolicyPaths: policyPaths, @@ -581,7 +752,7 @@ func (ar *AgentRunner) SendHeartbeat(ctx context.Context, staticAgentUUID uuid.U return nil } -func (ar *AgentRunner) getRunnerInstance(logger hclog.Logger, path string) (runner.Runner, error) { +func (ar *AgentRunner) getRunnerInstance(logger hclog.Logger, path string, protocolVersion int32) (runner.Runner, error) { // We're a host! Start by launching the plugin process. client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: runner.HandshakeConfig, @@ -598,15 +769,24 @@ func (ar *AgentRunner) getRunnerInstance(logger hclog.Logger, path string) (runn return nil, err } + dispenseName, err := runnerDispenseName(protocolVersion) + if err != nil { + return nil, err + } + // Request the plugin - raw, err := rpcClient.Dispense("runner") + logger.Debug("Dispensing plugin", "dispense_name", dispenseName) + raw, err := rpcClient.Dispense(dispenseName) if err != nil { return nil, err } // We should have a Greeter now! This feels like a normal interface // implementation but is in fact over an RPC connection. - runnerInstance := raw.(runner.Runner) + runnerInstance, ok := raw.(runner.Runner) + if !ok { + return nil, fmt.Errorf("dispensed plugin %q does not implement runner.Runner", dispenseName) + } return runnerInstance, nil } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 3555c46..e06d84a 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -2,9 +2,12 @@ package cmd import ( "bytes" + "context" + "errors" "fmt" "testing" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/spf13/viper" ) @@ -40,6 +43,30 @@ plugins: configYamlContent: ` api: url: http://localhost:8080 +`, + valid: false, + }, + { + name: "Unsupported Explicit Protocol Version", + configYamlContent: ` +api: + url: http://localhost:8080 + +plugins: + test-plugin: + source: ghcr.io/some-plugin:v1 + protocol_version: 100 +`, + valid: false, + }, + { + name: "Null Plugin Configuration", + configYamlContent: ` +api: + url: http://localhost:8080 + +plugins: + test-plugin: null `, valid: false, }, @@ -118,3 +145,330 @@ func TestAgentCmd_ConfigurationMerging(t *testing.T) { } }) } + +func TestMergeConfig_DefaultsPluginProtocolVersion(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(bytes.NewBufferString("api:\n url: http://localhost:8080\n\nplugins:\n plugin-with-default:\n source: ghcr.io/some-plugin:v1\n plugin-with-explicit:\n source: ghcr.io/some-plugin:v2\n protocol_version: 2\n")) + if err != nil { + t.Fatalf("Error reading config: %v", err) + } + + config, err := mergeConfig(AgentCmd(), v) + if err != nil { + t.Fatalf("Error merging config: %v", err) + } + + if got := config.Plugins["plugin-with-default"].ProtocolVersion; got != 1 { + t.Fatalf("Expected plugin-with-default protocol version to be 1, got %d", got) + } + + if got := config.Plugins["plugin-with-explicit"].ProtocolVersion; got != 2 { + t.Fatalf("Expected plugin-with-explicit protocol version to be 2, got %d", got) + } +} + +func TestUpdateAllPluginProtocols_DefaultsOnlyUnset(t *testing.T) { + config := &agentConfig{ + Plugins: map[string]*agentPlugin{ + "defaulted": { + Source: "ghcr.io/defaulted:v1", + }, + "explicit": { + Source: "ghcr.io/explicit:v2", + ProtocolVersion: 2, + protocolSet: true, + }, + "explicit-zero": { + Source: "ghcr.io/explicit-zero:v1", + ProtocolVersion: 0, + protocolSet: true, + }, + }, + } + + updateAllPluginProtocols(config) + + if got := config.Plugins["defaulted"].ProtocolVersion; got != 1 { + t.Fatalf("Expected defaulted plugin protocol version to be 1, got %d", got) + } + + if got := config.Plugins["explicit"].ProtocolVersion; got != 2 { + t.Fatalf("Expected explicit plugin protocol version to remain 2, got %d", got) + } + + if got := config.Plugins["explicit-zero"].ProtocolVersion; got != 0 { + t.Fatalf("Expected explicit-zero plugin protocol version to remain 0, got %d", got) + } +} + +func TestMergeConfig_RejectsUnsupportedExplicitProtocolVersion(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(bytes.NewBufferString("api:\n url: http://localhost:8080\n\nplugins:\n plugin-with-invalid-version:\n source: ghcr.io/some-plugin:v1\n protocol_version: 100\n")) + if err != nil { + t.Fatalf("Error reading config: %v", err) + } + + config, err := mergeConfig(AgentCmd(), v) + if err != nil { + t.Fatalf("Error merging config: %v", err) + } + + err = config.validate() + if err == nil { + t.Fatalf("Expected config validation to fail for unsupported protocol version") + } + + expected := "plugin plugin-with-invalid-version has unsupported protocol_version=100; supported values are 1 and 2" + if err.Error() != expected { + t.Fatalf("Expected error %q, got %q", expected, err.Error()) + } +} + +func TestMergeConfig_RejectsExplicitZeroProtocolVersion(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(bytes.NewBufferString("api:\n url: http://localhost:8080\n\nplugins:\n plugin-with-zero-version:\n source: ghcr.io/some-plugin:v1\n protocol_version: 0\n")) + if err != nil { + t.Fatalf("Error reading config: %v", err) + } + + config, err := mergeConfig(AgentCmd(), v) + if err != nil { + t.Fatalf("Error merging config: %v", err) + } + + err = config.validate() + if err == nil { + t.Fatalf("Expected config validation to fail for explicit zero protocol version") + } + + expected := "plugin plugin-with-zero-version has unsupported protocol_version=0; supported values are 1 and 2" + if err.Error() != expected { + t.Fatalf("Expected error %q, got %q", expected, err.Error()) + } +} + +func TestMergeConfig_RejectsNullPluginConfiguration(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(bytes.NewBufferString("api:\n url: http://localhost:8080\n\nplugins:\n null-plugin: null\n")) + if err != nil { + t.Fatalf("Error reading config: %v", err) + } + + config, err := mergeConfig(AgentCmd(), v) + if err != nil { + t.Fatalf("Error merging config: %v", err) + } + + err = config.validate() + if err == nil { + t.Fatalf("Expected config validation to fail for null plugin configuration") + } + + expected := "plugin null-plugin has null configuration" + if err.Error() != expected { + t.Fatalf("Expected error %q, got %q", expected, err.Error()) + } +} + +func TestMergeConfig_DoesNotFetchAnnotations(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(bytes.NewBufferString("api:\n url: http://localhost:8080\n\nplugins:\n plugin-with-default:\n source: ghcr.io/some-plugin:v1\n")) + if err != nil { + t.Fatalf("Error reading config: %v", err) + } + + config, err := mergeConfig(AgentCmd(), v) + if err != nil { + t.Fatalf("Error merging config: %v", err) + } + + if got := config.Plugins["plugin-with-default"].ProtocolVersion; got != DefaultProtocolVersion { + t.Fatalf("Expected plugin-with-default protocol version to be %d, got %d", DefaultProtocolVersion, got) + } +} + +func TestResolvePluginProtocols_UsesAnnotationsOnlyForImplicitOCIPlugins(t *testing.T) { + lookupCount := 0 + ctx := context.Background() + fetchAnnotations := func(fetchCtx context.Context, source string, option ...remote.Option) (map[string]string, error) { + lookupCount++ + if fetchCtx == nil { + t.Fatalf("expected fetchAnnotations context to be set") + } + return map[string]string{ + AnnotationProtocolVersionKey: "2", + }, nil + } + + config := &agentConfig{ + Plugins: map[string]*agentPlugin{ + "implicit-oci": { + Source: "ghcr.io/implicit:v1", + ProtocolVersion: DefaultProtocolVersion, + protocolSet: false, + }, + "explicit-v1": { + Source: "ghcr.io/explicit:v1", + ProtocolVersion: DefaultProtocolVersion, + protocolSet: true, + }, + "non-oci": { + Source: "/tmp/plugin", + ProtocolVersion: DefaultProtocolVersion, + protocolSet: false, + }, + }, + } + + runner := NewAgentRunner() + runner.fetchAnnotations = fetchAnnotations + runner.UpdateConfig(config) + runner.resolvePluginProtocols(ctx) + + if lookupCount != 1 { + t.Fatalf("Expected one annotation lookup, got %d", lookupCount) + } + + if got := config.Plugins["implicit-oci"].ProtocolVersion; got != RunnerV2ProtocolVersion { + t.Fatalf("Expected implicit-oci protocol version to be %d, got %d", RunnerV2ProtocolVersion, got) + } + + if got := config.Plugins["explicit-v1"].ProtocolVersion; got != DefaultProtocolVersion { + t.Fatalf("Expected explicit-v1 protocol version to remain %d, got %d", DefaultProtocolVersion, got) + } + + if got := config.Plugins["non-oci"].ProtocolVersion; got != DefaultProtocolVersion { + t.Fatalf("Expected non-oci protocol version to remain %d, got %d", DefaultProtocolVersion, got) + } +} + +func TestResolvePluginProtocols_KeepsDefaultWhenLookupFails(t *testing.T) { + fetchAnnotations := func(fetchCtx context.Context, source string, option ...remote.Option) (map[string]string, error) { + if fetchCtx == nil { + t.Fatalf("expected fetchAnnotations context to be set") + } + return nil, errors.New("lookup failed") + } + + config := &agentConfig{ + Plugins: map[string]*agentPlugin{ + "implicit-oci": { + Source: "ghcr.io/implicit:v1", + ProtocolVersion: DefaultProtocolVersion, + protocolSet: false, + }, + }, + } + + runner := NewAgentRunner() + runner.fetchAnnotations = fetchAnnotations + runner.UpdateConfig(config) + runner.resolvePluginProtocols(context.Background()) + + if got := config.Plugins["implicit-oci"].ProtocolVersion; got != DefaultProtocolVersion { + t.Fatalf("Expected implicit-oci protocol version to remain %d, got %d", DefaultProtocolVersion, got) + } +} + +func TestProtocolVersionFromAnnotations(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expected int32 + ok bool + }{ + { + name: "Uses OCI annotation key", + annotations: map[string]string{ + AnnotationProtocolVersionKey: "2", + }, + expected: 2, + ok: true, + }, + { + name: "Rejects unsupported values", + annotations: map[string]string{ + AnnotationProtocolVersionKey: "100", + }, + expected: 0, + ok: false, + }, + { + name: "Rejects invalid values", + annotations: map[string]string{ + AnnotationProtocolVersionKey: "abc", + }, + expected: 0, + ok: false, + }, + { + name: "Rejects non-positive values", + annotations: map[string]string{ + AnnotationProtocolVersionKey: "0", + }, + expected: 0, + ok: false, + }, + { + name: "Missing keys", + annotations: map[string]string{"other": "1"}, + expected: 0, + ok: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := protocolVersionFromAnnotations(tt.annotations) + if got != tt.expected || ok != tt.ok { + t.Fatalf("protocolVersionFromAnnotations() = (%d, %t), expected (%d, %t)", got, ok, tt.expected, tt.ok) + } + }) + } +} + +func TestRunnerDispenseName(t *testing.T) { + tests := []struct { + name string + protocolVersion int32 + expected string + wantErr bool + }{ + { + name: "Uses runner for v1", + protocolVersion: DefaultProtocolVersion, + expected: "runner", + wantErr: false, + }, + { + name: "Uses runner-v2 for v2", + protocolVersion: RunnerV2ProtocolVersion, + expected: "runner-v2", + wantErr: false, + }, + { + name: "Rejects unsupported protocol version", + protocolVersion: 3, + expected: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := runnerDispenseName(tt.protocolVersion) + if (err != nil) != tt.wantErr { + t.Fatalf("runnerDispenseName() error = %v, wantErr %t", err, tt.wantErr) + } + + if got != tt.expected { + t.Fatalf("runnerDispenseName() = %q, expected %q", got, tt.expected) + } + }) + } +} diff --git a/docs/adr/0001-support-versioned-runner-plugins.md b/docs/adr/0001-support-versioned-runner-plugins.md new file mode 100644 index 0000000..ddcdf4a --- /dev/null +++ b/docs/adr/0001-support-versioned-runner-plugins.md @@ -0,0 +1,42 @@ +# ADR 0001: Support versioned runner plugins + +- Date: 2026-03-10 + +## Context + +The agent currently assumes every plugin speaks a single runner protocol and can be started through the `runner` dispense name and evaluated immediately. + +The agent now supports a second plugin contract that requires an `Init` step before `Eval`. At the same time, it remains compatible with existing plugins and avoids forcing every deployment to update configuration when an OCI-published plugin can already advertise its protocol version. + +## Decision + +The agent supports explicit runner protocol versions per plugin and retains backward compatibility by defaulting to protocol version 1. + +This is implemented by: + +- adding `protocol_version` to plugin configuration +- defaulting unspecified plugins to protocol version 1 +- reading `org.ccf.plugin.protocol.version` from OCI annotations for OCI plugin sources without an explicit `protocol_version` + - this currently applies to tag-form OCI references such as `ghcr.io/example/plugin:v1` + - digest-form references such as `ghcr.io/example/plugin@sha256:...` are not currently treated as supported OCI download sources by the agent +- supporting only protocol versions 1 and 2 +- mapping protocol version 1 to the `runner` dispense name and protocol version 2 to `runner-v2` +- calling `Init` before `Eval` for protocol version 2 plugins +- treating explicit configuration as authoritative over OCI metadata + +## Consequences + +### Positive + +- Existing plugins continue to work without configuration changes. +- New plugins can adopt protocol version 2 and perform setup during `Init`. +- OCI-published plugins can self-describe their protocol version, reducing configuration drift. +- The supported OCI source shape is explicit: tag-form references participate in annotation lookup and download. +- Unsupported or invalid annotations do not break execution; the agent logs and falls back to the configured or default version. + +### Negative + +- OCI-backed plugins may require an extra registry metadata lookup before execution. +- Digest-form OCI references are not currently supported for plugin download or annotation-based protocol resolution. +- The agent now maintains two supported runner contracts instead of one. +- Plugin authors adopting protocol version 2 must implement `Init`. diff --git a/internal/oci.go b/internal/oci.go index ad3f674..d1fcd51 100644 --- a/internal/oci.go +++ b/internal/oci.go @@ -2,21 +2,74 @@ package internal import ( "context" + "encoding/json" "errors" + "os" + "path" + "github.com/compliance-framework/gooci/pkg/oci" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/hashicorp/go-hclog" - "os" - "path" ) func IsOCI(source string) bool { - // Check whether this can be parsed as an OCI endpoint + // Check whether this can be parsed as an OCI tag, which is what our downloader supports. _, err := name.NewTag(source, name.StrictValidation) return err == nil } +func GetAnnotations(ctx context.Context, source string, option ...remote.Option) (map[string]string, error) { + ref, err := name.ParseReference(source, name.StrictValidation) + if err != nil { + return nil, err + } + + opts := append([]remote.Option{ + remote.WithContext(ctx), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + }, option...) + + desc, err := remote.Get(ref, opts...) + if err != nil { + return nil, err + } + + return annotationsFromDescriptor(desc), nil +} + +func annotationsFromDescriptor(desc *remote.Descriptor) map[string]string { + if desc == nil { + return map[string]string{} + } + + if len(desc.Manifest) > 0 { + var payload struct { + Annotations map[string]string `json:"annotations"` + } + + if err := json.Unmarshal(desc.Manifest, &payload); err == nil && len(payload.Annotations) > 0 { + return copyAnnotations(payload.Annotations) + } + } + + if len(desc.Annotations) > 0 { + return copyAnnotations(desc.Annotations) + } + + return map[string]string{} +} + +func copyAnnotations(in map[string]string) map[string]string { + out := make(map[string]string, len(in)) + for key, value := range in { + out[key] = value + } + + return out +} + func Download(ctx context.Context, source string, outputDir string, binaryPath string, logger hclog.Logger, option ...remote.Option) (string, error) { // Add a task to indicate we've downloaded the items logger.Trace("Checking for source", "source", source) diff --git a/internal/oci_manifest_test.go b/internal/oci_manifest_test.go new file mode 100644 index 0000000..7388d0f --- /dev/null +++ b/internal/oci_manifest_test.go @@ -0,0 +1,98 @@ +package internal + +import ( + "reflect" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func TestAnnotationsFromDescriptor(t *testing.T) { + tests := []struct { + name string + desc *remote.Descriptor + expected map[string]string + }{ + { + name: "Nil descriptor", + desc: nil, + expected: map[string]string{}, + }, + { + name: "Invalid JSON falls back to descriptor annotations", + desc: &remote.Descriptor{ + Manifest: []byte("not-json"), + Descriptor: v1.Descriptor{ + Annotations: map[string]string{"from": "descriptor"}, + }, + }, + expected: map[string]string{"from": "descriptor"}, + }, + { + name: "Manifest without annotations falls back to descriptor annotations", + desc: &remote.Descriptor{ + Manifest: []byte(`{"schemaVersion":2}`), + Descriptor: v1.Descriptor{ + Annotations: map[string]string{"from": "descriptor"}, + }, + }, + expected: map[string]string{"from": "descriptor"}, + }, + { + name: "Uses manifest annotations when present", + desc: &remote.Descriptor{ + Manifest: []byte(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[],"annotations":{"org.opencontainers.image.created":"2026-02-27T10:57:27Z","org.opencontainers.image.title":"plugin-test","org.opencontainers.image.version":"v0.1.0","org.ccf.plugin.protocol.version":"2"}}`), + Descriptor: v1.Descriptor{ + Annotations: map[string]string{"from": "descriptor"}, + }, + }, + expected: map[string]string{ + "org.opencontainers.image.created": "2026-02-27T10:57:27Z", + "org.opencontainers.image.title": "plugin-test", + "org.opencontainers.image.version": "v0.1.0", + "org.ccf.plugin.protocol.version": "2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := annotationsFromDescriptor(tt.desc) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("annotationsFromDescriptor() = %v, expected %v", got, tt.expected) + } + }) + } +} + +func TestAnnotationsFromDescriptor_ReturnsDefensiveCopy(t *testing.T) { + t.Run("Descriptor annotations are copied", func(t *testing.T) { + desc := &remote.Descriptor{ + Descriptor: v1.Descriptor{ + Annotations: map[string]string{"from": "descriptor"}, + }, + } + + got := annotationsFromDescriptor(desc) + got["from"] = "modified" + + if desc.Annotations["from"] != "descriptor" { + t.Fatalf("expected descriptor annotations to remain unchanged, got %q", desc.Annotations["from"]) + } + }) + + t.Run("Manifest annotations are copied", func(t *testing.T) { + desc := &remote.Descriptor{ + Manifest: []byte(`{"schemaVersion":2,"annotations":{"org.ccf.plugin.protocol.version":"2"}}`), + } + + got := annotationsFromDescriptor(desc) + got["org.ccf.plugin.protocol.version"] = "1" + + again := annotationsFromDescriptor(desc) + if again["org.ccf.plugin.protocol.version"] != "2" { + t.Fatalf("expected manifest annotations to remain unchanged, got %q", again["org.ccf.plugin.protocol.version"]) + } + }) +} diff --git a/internal/utils_test.go b/internal/utils_test.go index eaf3f75..b402ad5 100644 --- a/internal/utils_test.go +++ b/internal/utils_test.go @@ -86,6 +86,11 @@ func TestIsOci(t *testing.T) { source: "docker.io/library/alpine:latest", expected: true, }, + { + name: "Digest OCI reference is not treated as supported OCI tag", + source: "ghcr.io/example/plugin@sha256:88252198a40099248f5cc3272bc879fade8b7001a2bcb36d7b43aa8f54328714", + expected: false, + }, { name: "Tar artifact", source: "docker.io/library/alpine.tar.gz", diff --git a/runner/grpc.go b/runner/grpc.go index 649999e..14cce6e 100644 --- a/runner/grpc.go +++ b/runner/grpc.go @@ -2,10 +2,14 @@ package runner import ( "context" + "sync" + "github.com/compliance-framework/agent/runner/proto" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type ApiHelper interface { @@ -25,43 +29,79 @@ func (m *GRPCApiHelperClient) CreateEvidence(ctx context.Context, evidence []*pr } type GRPCApiHelperServer struct { + mu sync.RWMutex + // This is the real implementation Impl ApiHelper } +func (m *GRPCApiHelperServer) SetImpl(impl ApiHelper) { + m.mu.Lock() + defer m.mu.Unlock() + m.Impl = impl +} + func (m *GRPCApiHelperServer) CreateEvidence(ctx context.Context, req *proto.CreateEvidenceRequest) (resp *proto.CreateEvidenceResponse, err error) { - err = m.Impl.CreateEvidence(ctx, req.GetEvidence()) + m.mu.RLock() + impl := m.Impl + m.mu.RUnlock() + if impl == nil { + return nil, status.Error(codes.FailedPrecondition, "API helper server is not configured") + } + + err = impl.CreateEvidence(ctx, req.GetEvidence()) if err != nil { return nil, err } return &proto.CreateEvidenceResponse{}, err } -// GRPCClient is an implementation of KV that talks over RPC. +// GRPCClient implements Runner over go-plugin gRPC. type GRPCClient struct { client proto.RunnerClient broker *plugin.GRPCBroker + + apiHelperServer *GRPCApiHelperServer + apiServerID uint32 + apiServerOnce sync.Once } -func (m *GRPCClient) Configure(request *proto.ConfigureRequest) (*proto.ConfigureResponse, error) { - return m.client.Configure(context.Background(), request) +// GRPCClientV2 extends GRPCClient with RunnerV2 support over go-plugin gRPC. +type GRPCClientV2 struct { + *GRPCClient } -func (m *GRPCClient) Eval(request *proto.EvalRequest, a ApiHelper) (*proto.EvalResponse, error) { - apiHelperServer := &GRPCApiHelperServer{Impl: a} +func (m *GRPCClient) startAPIServer(a ApiHelper) uint32 { + m.apiServerOnce.Do(func() { + m.apiHelperServer = &GRPCApiHelperServer{} - var s *grpc.Server - serverFunc := func(opts []grpc.ServerOption) *grpc.Server { - s = grpc.NewServer(opts...) - proto.RegisterApiHelperServer(s, apiHelperServer) + serverFunc := func(opts []grpc.ServerOption) *grpc.Server { + s := grpc.NewServer(opts...) + proto.RegisterApiHelperServer(s, m.apiHelperServer) + return s + } - return s - } + m.apiServerID = m.broker.NextId() + go m.broker.AcceptAndServe(m.apiServerID, serverFunc) + }) + + m.apiHelperServer.SetImpl(a) - brokerID := m.broker.NextId() - go m.broker.AcceptAndServe(brokerID, serverFunc) + return m.apiServerID +} - request.ApiServer = brokerID +func (m *GRPCClient) Configure(request *proto.ConfigureRequest) (*proto.ConfigureResponse, error) { + return m.client.Configure(context.Background(), request) +} + +func (m *GRPCClientV2) Init(request *proto.InitRequest, a ApiHelper) (*proto.InitResponse, error) { + request.ApiServer = m.startAPIServer(a) + resp, err := m.client.Init(context.Background(), request) + return resp, err +} + +func (m *GRPCClient) Eval(request *proto.EvalRequest, a ApiHelper) (*proto.EvalResponse, error) { + request.ApiServer = m.startAPIServer(a) resp, err := m.client.Eval(context.Background(), request) return resp, err } @@ -75,6 +115,22 @@ func (m *GRPCServer) Configure(ctx context.Context, req *proto.ConfigureRequest) return m.Impl.Configure(req) } +func (m *GRPCServer) Init(ctx context.Context, req *proto.InitRequest) (*proto.InitResponse, error) { + runnerV2, ok := m.Impl.(RunnerV2) + if !ok { + return nil, status.Error(codes.Unimplemented, "Init is only supported for protocol v2 plugins") + } + + conn, err := m.broker.Dial(req.ApiServer) + if err != nil { + return nil, err + } + defer conn.Close() + + a := &GRPCApiHelperClient{proto.NewApiHelperClient(conn)} + return runnerV2.Init(req, a) +} + func (m *GRPCServer) Eval(ctx context.Context, req *proto.EvalRequest) (*proto.EvalResponse, error) { conn, err := m.broker.Dial(req.ApiServer) if err != nil { diff --git a/runner/grpc_test.go b/runner/grpc_test.go new file mode 100644 index 0000000..5d5e2ce --- /dev/null +++ b/runner/grpc_test.go @@ -0,0 +1,45 @@ +package runner + +import ( + "context" + "testing" + + "github.com/compliance-framework/agent/runner/proto" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type testRunnerV1 struct{} + +func (t *testRunnerV1) Configure(request *proto.ConfigureRequest) (*proto.ConfigureResponse, error) { + return &proto.ConfigureResponse{}, nil +} + +func (t *testRunnerV1) Eval(request *proto.EvalRequest, a ApiHelper) (*proto.EvalResponse, error) { + return &proto.EvalResponse{}, nil +} + +func TestGRPCServerInitReturnsUnimplementedForRunnerV1(t *testing.T) { + server := &GRPCServer{Impl: &testRunnerV1{}} + + _, err := server.Init(context.Background(), &proto.InitRequest{}) + if err == nil { + t.Fatalf("expected error, got nil") + } + + if status.Code(err) != codes.Unimplemented { + t.Fatalf("expected code %v, got %v", codes.Unimplemented, status.Code(err)) + } +} + +func TestGRPCClientCapabilitiesMatchProtocolVersion(t *testing.T) { + v1Client := &GRPCClient{} + if _, ok := interface{}(v1Client).(RunnerV2); ok { + t.Fatalf("expected v1 gRPC client to not implement RunnerV2") + } + + v2Client := &GRPCClientV2{GRPCClient: &GRPCClient{}} + if _, ok := interface{}(v2Client).(RunnerV2); !ok { + t.Fatalf("expected v2 gRPC client to implement RunnerV2") + } +} diff --git a/runner/plugin.go b/runner/plugin.go index 102ddc5..d49ecb9 100644 --- a/runner/plugin.go +++ b/runner/plugin.go @@ -13,6 +13,11 @@ type Runner interface { Eval(request *proto.EvalRequest, a ApiHelper) (*proto.EvalResponse, error) } +type RunnerV2 interface { + Runner + Init(request *proto.InitRequest, a ApiHelper) (*proto.InitResponse, error) +} + type RunnerGRPCPlugin struct { plugin.Plugin @@ -20,6 +25,13 @@ type RunnerGRPCPlugin struct { Impl Runner } +type RunnerV2GRPCPlugin struct { + plugin.Plugin + + // Impl Injection + Impl RunnerV2 +} + func (p *RunnerGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { proto.RegisterRunnerServer(s, &GRPCServer{ Impl: p.Impl, @@ -35,6 +47,23 @@ func (p *RunnerGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBr }, nil } +func (p *RunnerV2GRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterRunnerServer(s, &GRPCServer{ + Impl: p.Impl, + broker: broker, + }) + return nil +} + +func (p *RunnerV2GRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &GRPCClientV2{ + GRPCClient: &GRPCClient{ + client: proto.NewRunnerClient(c), + broker: broker, + }, + }, nil +} + var HandshakeConfig = plugin.HandshakeConfig{ ProtocolVersion: 1, MagicCookieKey: "RUNNER_PLUGIN", @@ -42,5 +71,6 @@ var HandshakeConfig = plugin.HandshakeConfig{ } var PluginMap = map[string]plugin.Plugin{ - "runner": &RunnerGRPCPlugin{}, + "runner": &RunnerGRPCPlugin{}, + "runner-v2": &RunnerV2GRPCPlugin{}, } diff --git a/runner/proto/results.pb.go b/runner/proto/results.pb.go index d151dd5..502d6b0 100644 --- a/runner/proto/results.pb.go +++ b/runner/proto/results.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: runner/proto/results.proto diff --git a/runner/proto/runner.pb.go b/runner/proto/runner.pb.go index 6b2a93f..bb34207 100644 --- a/runner/proto/runner.pb.go +++ b/runner/proto/runner.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: runner/proto/runner.proto @@ -155,6 +155,94 @@ func (x *ConfigureResponse) GetValue() []byte { return nil } +type InitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PolicyPaths []string `protobuf:"bytes,1,rep,name=policyPaths,proto3" json:"policyPaths,omitempty"` + ApiServer uint32 `protobuf:"varint,2,opt,name=apiServer,proto3" json:"apiServer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InitRequest) Reset() { + *x = InitRequest{} + mi := &file_runner_proto_runner_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitRequest) ProtoMessage() {} + +func (x *InitRequest) ProtoReflect() protoreflect.Message { + mi := &file_runner_proto_runner_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. +func (*InitRequest) Descriptor() ([]byte, []int) { + return file_runner_proto_runner_proto_rawDescGZIP(), []int{2} +} + +func (x *InitRequest) GetPolicyPaths() []string { + if x != nil { + return x.PolicyPaths + } + return nil +} + +func (x *InitRequest) GetApiServer() uint32 { + if x != nil { + return x.ApiServer + } + return 0 +} + +type InitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InitResponse) Reset() { + *x = InitResponse{} + mi := &file_runner_proto_runner_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitResponse) ProtoMessage() {} + +func (x *InitResponse) ProtoReflect() protoreflect.Message { + mi := &file_runner_proto_runner_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead. +func (*InitResponse) Descriptor() ([]byte, []int) { + return file_runner_proto_runner_proto_rawDescGZIP(), []int{3} +} + type EvalRequest struct { state protoimpl.MessageState `protogen:"open.v1"` PolicyPaths []string `protobuf:"bytes,1,rep,name=policyPaths,proto3" json:"policyPaths,omitempty"` @@ -165,7 +253,7 @@ type EvalRequest struct { func (x *EvalRequest) Reset() { *x = EvalRequest{} - mi := &file_runner_proto_runner_proto_msgTypes[2] + mi := &file_runner_proto_runner_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -177,7 +265,7 @@ func (x *EvalRequest) String() string { func (*EvalRequest) ProtoMessage() {} func (x *EvalRequest) ProtoReflect() protoreflect.Message { - mi := &file_runner_proto_runner_proto_msgTypes[2] + mi := &file_runner_proto_runner_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -190,7 +278,7 @@ func (x *EvalRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EvalRequest.ProtoReflect.Descriptor instead. func (*EvalRequest) Descriptor() ([]byte, []int) { - return file_runner_proto_runner_proto_rawDescGZIP(), []int{2} + return file_runner_proto_runner_proto_rawDescGZIP(), []int{4} } func (x *EvalRequest) GetPolicyPaths() []string { @@ -220,7 +308,7 @@ type EvalResponse struct { func (x *EvalResponse) Reset() { *x = EvalResponse{} - mi := &file_runner_proto_runner_proto_msgTypes[3] + mi := &file_runner_proto_runner_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -232,7 +320,7 @@ func (x *EvalResponse) String() string { func (*EvalResponse) ProtoMessage() {} func (x *EvalResponse) ProtoReflect() protoreflect.Message { - mi := &file_runner_proto_runner_proto_msgTypes[3] + mi := &file_runner_proto_runner_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -245,7 +333,7 @@ func (x *EvalResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EvalResponse.ProtoReflect.Descriptor instead. func (*EvalResponse) Descriptor() ([]byte, []int) { - return file_runner_proto_runner_proto_rawDescGZIP(), []int{3} + return file_runner_proto_runner_proto_rawDescGZIP(), []int{5} } func (x *EvalResponse) GetStatus() ExecutionStatus { @@ -267,6 +355,10 @@ const file_runner_proto_runner_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\")\n" + "\x11ConfigureResponse\x12\x14\n" + "\x05value\x18\x01 \x01(\fR\x05value\"M\n" + + "\vInitRequest\x12 \n" + + "\vpolicyPaths\x18\x01 \x03(\tR\vpolicyPaths\x12\x1c\n" + + "\tapiServer\x18\x02 \x01(\rR\tapiServer\"\x0e\n" + + "\fInitResponse\"M\n" + "\vEvalRequest\x12 \n" + "\vpolicyPaths\x18\x01 \x03(\tR\vpolicyPaths\x12\x1c\n" + "\tapiServer\x18\x02 \x01(\rR\tapiServer\">\n" + @@ -274,10 +366,11 @@ const file_runner_proto_runner_proto_rawDesc = "" + "\x06status\x18\x01 \x01(\x0e2\x16.proto.ExecutionStatusR\x06status*+\n" + "\x0fExecutionStatus\x12\v\n" + "\aSUCCESS\x10\x00\x12\v\n" + - "\aFAILURE\x10\x012y\n" + + "\aFAILURE\x10\x012\xaa\x01\n" + "\x06Runner\x12>\n" + "\tConfigure\x12\x17.proto.ConfigureRequest\x1a\x18.proto.ConfigureResponse\x12/\n" + - "\x04Eval\x12\x12.proto.EvalRequest\x1a\x13.proto.EvalResponseB\tZ\a./protob\x06proto3" + "\x04Eval\x12\x12.proto.EvalRequest\x1a\x13.proto.EvalResponse\x12/\n" + + "\x04Init\x12\x12.proto.InitRequest\x1a\x13.proto.InitResponseB\tZ\a./protob\x06proto3" var ( file_runner_proto_runner_proto_rawDescOnce sync.Once @@ -292,24 +385,28 @@ func file_runner_proto_runner_proto_rawDescGZIP() []byte { } var file_runner_proto_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_runner_proto_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_runner_proto_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_runner_proto_runner_proto_goTypes = []any{ (ExecutionStatus)(0), // 0: proto.ExecutionStatus (*ConfigureRequest)(nil), // 1: proto.ConfigureRequest (*ConfigureResponse)(nil), // 2: proto.ConfigureResponse - (*EvalRequest)(nil), // 3: proto.EvalRequest - (*EvalResponse)(nil), // 4: proto.EvalResponse - nil, // 5: proto.ConfigureRequest.ConfigEntry + (*InitRequest)(nil), // 3: proto.InitRequest + (*InitResponse)(nil), // 4: proto.InitResponse + (*EvalRequest)(nil), // 5: proto.EvalRequest + (*EvalResponse)(nil), // 6: proto.EvalResponse + nil, // 7: proto.ConfigureRequest.ConfigEntry } var file_runner_proto_runner_proto_depIdxs = []int32{ - 5, // 0: proto.ConfigureRequest.config:type_name -> proto.ConfigureRequest.ConfigEntry + 7, // 0: proto.ConfigureRequest.config:type_name -> proto.ConfigureRequest.ConfigEntry 0, // 1: proto.EvalResponse.status:type_name -> proto.ExecutionStatus 1, // 2: proto.Runner.Configure:input_type -> proto.ConfigureRequest - 3, // 3: proto.Runner.Eval:input_type -> proto.EvalRequest - 2, // 4: proto.Runner.Configure:output_type -> proto.ConfigureResponse - 4, // 5: proto.Runner.Eval:output_type -> proto.EvalResponse - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type + 5, // 3: proto.Runner.Eval:input_type -> proto.EvalRequest + 3, // 4: proto.Runner.Init:input_type -> proto.InitRequest + 2, // 5: proto.Runner.Configure:output_type -> proto.ConfigureResponse + 6, // 6: proto.Runner.Eval:output_type -> proto.EvalResponse + 4, // 7: proto.Runner.Init:output_type -> proto.InitResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name @@ -326,7 +423,7 @@ func file_runner_proto_runner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_runner_proto_runner_proto_rawDesc), len(file_runner_proto_runner_proto_rawDesc)), NumEnums: 1, - NumMessages: 5, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/runner/proto/runner.proto b/runner/proto/runner.proto index 9def54f..9d25118 100644 --- a/runner/proto/runner.proto +++ b/runner/proto/runner.proto @@ -16,6 +16,14 @@ message ConfigureResponse { bytes value = 1; } +message InitRequest { + repeated string policyPaths = 1; + uint32 apiServer = 2; +} + +message InitResponse { +} + message EvalRequest { repeated string policyPaths = 1; uint32 apiServer = 2; @@ -33,4 +41,5 @@ message EvalResponse { service Runner { rpc Configure(ConfigureRequest) returns (ConfigureResponse); rpc Eval(EvalRequest) returns (EvalResponse); + rpc Init(InitRequest) returns (InitResponse); } diff --git a/runner/proto/runner_grpc.pb.go b/runner/proto/runner_grpc.pb.go index efc9a51..116783c 100644 --- a/runner/proto/runner_grpc.pb.go +++ b/runner/proto/runner_grpc.pb.go @@ -21,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion7 const ( Runner_Configure_FullMethodName = "/proto.Runner/Configure" Runner_Eval_FullMethodName = "/proto.Runner/Eval" + Runner_Init_FullMethodName = "/proto.Runner/Init" ) // RunnerClient is the client API for Runner service. @@ -29,6 +30,7 @@ const ( type RunnerClient interface { Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) Eval(ctx context.Context, in *EvalRequest, opts ...grpc.CallOption) (*EvalResponse, error) + Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) } type runnerClient struct { @@ -57,12 +59,22 @@ func (c *runnerClient) Eval(ctx context.Context, in *EvalRequest, opts ...grpc.C return out, nil } +func (c *runnerClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) { + out := new(InitResponse) + err := c.cc.Invoke(ctx, Runner_Init_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RunnerServer is the server API for Runner service. // All implementations should embed UnimplementedRunnerServer // for forward compatibility type RunnerServer interface { Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) Eval(context.Context, *EvalRequest) (*EvalResponse, error) + Init(context.Context, *InitRequest) (*InitResponse, error) } // UnimplementedRunnerServer should be embedded to have forward compatible implementations. @@ -75,6 +87,9 @@ func (UnimplementedRunnerServer) Configure(context.Context, *ConfigureRequest) ( func (UnimplementedRunnerServer) Eval(context.Context, *EvalRequest) (*EvalResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Eval not implemented") } +func (UnimplementedRunnerServer) Init(context.Context, *InitRequest) (*InitResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Init not implemented") +} // UnsafeRunnerServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to RunnerServer will @@ -123,6 +138,24 @@ func _Runner_Eval_Handler(srv interface{}, ctx context.Context, dec func(interfa return interceptor(ctx, in, info, handler) } +func _Runner_Init_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InitRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RunnerServer).Init(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Runner_Init_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RunnerServer).Init(ctx, req.(*InitRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Runner_ServiceDesc is the grpc.ServiceDesc for Runner service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -138,6 +171,10 @@ var Runner_ServiceDesc = grpc.ServiceDesc{ MethodName: "Eval", Handler: _Runner_Eval_Handler, }, + { + MethodName: "Init", + Handler: _Runner_Init_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "runner/proto/runner.proto", diff --git a/runner/proto/types.pb.go b/runner/proto/types.pb.go index 3f34340..0d2a045 100644 --- a/runner/proto/types.pb.go +++ b/runner/proto/types.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: runner/proto/types.proto