mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
ab817b215c
Previously while passing the plugins's status to the Status API, the decision log plugin held the lock while a status upload was in process. So if a status upload took a while or the status plugin was blocked as it processed some other update, this would block policy evaluation requests received by the OPA server and increase client latency. This is because since the decision log plugin held the lock, new log events could be inserted into its buffer only after the lock was released. This change makes a copy of the decision log status so that the lock is not held during a status update. Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
548 lines
16 KiB
Go
548 lines
16 KiB
Go
// Copyright 2018 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package status implements status reporting.
|
|
package status
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
|
|
lstat "github.com/open-policy-agent/opa/plugins/logs/status"
|
|
prom "github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/open-policy-agent/opa/logging"
|
|
"github.com/open-policy-agent/opa/metrics"
|
|
"github.com/open-policy-agent/opa/plugins"
|
|
"github.com/open-policy-agent/opa/plugins/bundle"
|
|
"github.com/open-policy-agent/opa/util"
|
|
)
|
|
|
|
// Logger defines the interface for status plugins.
|
|
type Logger interface {
|
|
plugins.Plugin
|
|
|
|
Log(context.Context, *UpdateRequestV1) error
|
|
}
|
|
|
|
// UpdateRequestV1 represents the status update message that OPA sends to
|
|
// remote HTTP endpoints.
|
|
type UpdateRequestV1 struct {
|
|
Labels map[string]string `json:"labels"`
|
|
Bundle *bundle.Status `json:"bundle,omitempty"` // Deprecated: Use bulk `bundles` status updates instead
|
|
Bundles map[string]*bundle.Status `json:"bundles,omitempty"`
|
|
Discovery *bundle.Status `json:"discovery,omitempty"`
|
|
DecisionLogs *lstat.Status `json:"decision_logs,omitempty"`
|
|
Metrics map[string]interface{} `json:"metrics,omitempty"`
|
|
Plugins map[string]*plugins.Status `json:"plugins,omitempty"`
|
|
}
|
|
|
|
// Plugin implements status reporting. Updates can be triggered by the caller.
|
|
type Plugin struct {
|
|
manager *plugins.Manager
|
|
config Config
|
|
bundleCh chan bundle.Status // Deprecated: Use bulk bundle status updates instead
|
|
lastBundleStatus *bundle.Status // Deprecated: Use bulk bundle status updates instead
|
|
bulkBundleCh chan map[string]*bundle.Status
|
|
lastBundleStatuses map[string]*bundle.Status
|
|
discoCh chan bundle.Status
|
|
lastDiscoStatus *bundle.Status
|
|
pluginStatusCh chan map[string]*plugins.Status
|
|
decisionLogsCh chan lstat.Status
|
|
lastDecisionLogsStatus *lstat.Status
|
|
lastPluginStatuses map[string]*plugins.Status
|
|
queryCh chan chan *UpdateRequestV1
|
|
stop chan chan struct{}
|
|
reconfig chan reconfigure
|
|
metrics metrics.Metrics
|
|
logger logging.Logger
|
|
trigger chan trigger
|
|
}
|
|
|
|
// Config contains configuration for the plugin.
|
|
type Config struct {
|
|
Plugin *string `json:"plugin"`
|
|
Service string `json:"service"`
|
|
PartitionName string `json:"partition_name,omitempty"`
|
|
ConsoleLogs bool `json:"console"`
|
|
Prometheus bool `json:"prometheus"`
|
|
Trigger *plugins.TriggerMode `json:"trigger,omitempty"` // trigger mode
|
|
}
|
|
|
|
type reconfigure struct {
|
|
config interface{}
|
|
done chan struct{}
|
|
}
|
|
|
|
type trigger struct {
|
|
ctx context.Context
|
|
done chan error
|
|
}
|
|
|
|
func (c *Config) validateAndInjectDefaults(services []string, pluginsList []string, trigger *plugins.TriggerMode) error {
|
|
|
|
if c.Plugin != nil {
|
|
var found bool
|
|
for _, other := range pluginsList {
|
|
if other == *c.Plugin {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("invalid plugin name %q in status", *c.Plugin)
|
|
}
|
|
} else if c.Service == "" && len(services) != 0 && !(c.ConsoleLogs || c.Prometheus) {
|
|
// For backwards compatibility allow defaulting to the first
|
|
// service listed, but only if console logging is disabled. If enabled
|
|
// we can't tell if the deployer wanted to use only console logs or
|
|
// both console logs and the default service option.
|
|
c.Service = services[0]
|
|
} else if c.Service != "" {
|
|
found := false
|
|
|
|
for _, svc := range services {
|
|
if svc == c.Service {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("invalid service name %q in status", c.Service)
|
|
}
|
|
}
|
|
|
|
t, err := plugins.ValidateAndInjectDefaultsForTriggerMode(trigger, c.Trigger)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid status config: %w", err)
|
|
}
|
|
c.Trigger = t
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseConfig validates the config and injects default values.
|
|
func ParseConfig(config []byte, services []string, pluginsList []string) (*Config, error) {
|
|
t := plugins.DefaultTriggerMode
|
|
return NewConfigBuilder().WithBytes(config).WithServices(services).WithPlugins(pluginsList).WithTriggerMode(&t).Parse()
|
|
}
|
|
|
|
// ConfigBuilder assists in the construction of the plugin configuration.
|
|
type ConfigBuilder struct {
|
|
raw []byte
|
|
services []string
|
|
plugins []string
|
|
trigger *plugins.TriggerMode
|
|
}
|
|
|
|
// NewConfigBuilder returns a new ConfigBuilder to build and parse the plugin config.
|
|
func NewConfigBuilder() *ConfigBuilder {
|
|
return &ConfigBuilder{}
|
|
}
|
|
|
|
// WithBytes sets the raw plugin config.
|
|
func (b *ConfigBuilder) WithBytes(config []byte) *ConfigBuilder {
|
|
b.raw = config
|
|
return b
|
|
}
|
|
|
|
// WithServices sets the services that implement control plane APIs.
|
|
func (b *ConfigBuilder) WithServices(services []string) *ConfigBuilder {
|
|
b.services = services
|
|
return b
|
|
}
|
|
|
|
// WithPlugins sets the list of named plugins for status updates.
|
|
func (b *ConfigBuilder) WithPlugins(plugins []string) *ConfigBuilder {
|
|
b.plugins = plugins
|
|
return b
|
|
}
|
|
|
|
// WithTriggerMode sets the plugin trigger mode.
|
|
func (b *ConfigBuilder) WithTriggerMode(trigger *plugins.TriggerMode) *ConfigBuilder {
|
|
b.trigger = trigger
|
|
return b
|
|
}
|
|
|
|
// Parse validates the config and injects default values.
|
|
func (b *ConfigBuilder) Parse() (*Config, error) {
|
|
if b.raw == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var parsedConfig Config
|
|
|
|
if err := util.Unmarshal(b.raw, &parsedConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if parsedConfig.Plugin == nil && parsedConfig.Service == "" && len(b.services) == 0 && !parsedConfig.ConsoleLogs && !parsedConfig.Prometheus {
|
|
// Nothing to validate or inject
|
|
return nil, nil
|
|
}
|
|
|
|
if err := parsedConfig.validateAndInjectDefaults(b.services, b.plugins, b.trigger); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &parsedConfig, nil
|
|
}
|
|
|
|
// New returns a new Plugin with the given config.
|
|
func New(parsedConfig *Config, manager *plugins.Manager) *Plugin {
|
|
p := &Plugin{
|
|
manager: manager,
|
|
config: *parsedConfig,
|
|
bundleCh: make(chan bundle.Status),
|
|
bulkBundleCh: make(chan map[string]*bundle.Status),
|
|
discoCh: make(chan bundle.Status),
|
|
decisionLogsCh: make(chan lstat.Status),
|
|
stop: make(chan chan struct{}),
|
|
reconfig: make(chan reconfigure),
|
|
pluginStatusCh: make(chan map[string]*plugins.Status),
|
|
queryCh: make(chan chan *UpdateRequestV1),
|
|
logger: manager.Logger().WithFields(map[string]interface{}{"plugin": Name}),
|
|
trigger: make(chan trigger),
|
|
}
|
|
|
|
p.manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateNotReady})
|
|
|
|
return p
|
|
}
|
|
|
|
// WithMetrics sets the global metrics provider to be used by the plugin.
|
|
func (p *Plugin) WithMetrics(m metrics.Metrics) *Plugin {
|
|
p.metrics = m
|
|
return p
|
|
}
|
|
|
|
// Name identifies the plugin on manager.
|
|
const Name = "status"
|
|
|
|
// Lookup returns the status plugin registered with the manager.
|
|
func Lookup(manager *plugins.Manager) *Plugin {
|
|
if p := manager.Plugin(Name); p != nil {
|
|
return p.(*Plugin)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Start starts the plugin.
|
|
func (p *Plugin) Start(ctx context.Context) error {
|
|
p.logger.Info("Starting status reporter.")
|
|
|
|
go p.loop()
|
|
|
|
// Setup a listener for plugin statuses, but only after starting the loop
|
|
// to prevent blocking threads pushing the plugin updates.
|
|
p.manager.RegisterPluginStatusListener(Name, p.UpdatePluginStatus)
|
|
|
|
if p.config.Prometheus {
|
|
p.registerAll()
|
|
}
|
|
|
|
// Set the status plugin's status to OK now that everything is registered and
|
|
// the loop is running. This will trigger an update on the listener with the
|
|
// current status of all the other plugins too.
|
|
p.manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateOK})
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) register(r prom.Registerer, cs ...prom.Collector) {
|
|
for _, c := range cs {
|
|
if err := r.Register(c); err != nil {
|
|
p.logger.Error("Status metric failed to register on prometheus :%v.", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) registerAll() {
|
|
if p.manager.PrometheusRegister() != nil {
|
|
p.register(p.manager.PrometheusRegister(), allCollectors...)
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) unregister(r prom.Registerer, cs ...prom.Collector) {
|
|
for _, c := range cs {
|
|
r.Unregister(c)
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) unregisterAll() {
|
|
if p.manager.PrometheusRegister() != nil {
|
|
p.unregister(p.manager.PrometheusRegister(), allCollectors...)
|
|
}
|
|
}
|
|
|
|
// Stop stops the plugin.
|
|
func (p *Plugin) Stop(ctx context.Context) {
|
|
p.logger.Info("Stopping status reporter.")
|
|
p.manager.UnregisterPluginStatusListener(Name)
|
|
done := make(chan struct{})
|
|
p.stop <- done
|
|
<-done
|
|
p.manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateNotReady})
|
|
}
|
|
|
|
// UpdateBundleStatus notifies the plugin that the policy bundle was updated.
|
|
// Deprecated: Use BulkUpdateBundleStatus instead.
|
|
func (p *Plugin) UpdateBundleStatus(status bundle.Status) {
|
|
p.bundleCh <- status
|
|
}
|
|
|
|
// BulkUpdateBundleStatus notifies the plugin that the policy bundle was updated.
|
|
func (p *Plugin) BulkUpdateBundleStatus(status map[string]*bundle.Status) {
|
|
p.bulkBundleCh <- status
|
|
}
|
|
|
|
// UpdateDiscoveryStatus notifies the plugin that the discovery bundle was updated.
|
|
func (p *Plugin) UpdateDiscoveryStatus(status bundle.Status) {
|
|
p.discoCh <- status
|
|
}
|
|
|
|
// UpdateDecisionLogsStatus notifies the plugin that status of a decision log upload event.
|
|
func (p *Plugin) UpdateDecisionLogsStatus(status lstat.Status) {
|
|
p.decisionLogsCh <- status
|
|
}
|
|
|
|
// UpdatePluginStatus notifies the plugin that a plugin status was updated.
|
|
func (p *Plugin) UpdatePluginStatus(status map[string]*plugins.Status) {
|
|
p.pluginStatusCh <- status
|
|
}
|
|
|
|
// Reconfigure notifies the plugin with a new configuration.
|
|
func (p *Plugin) Reconfigure(_ context.Context, config interface{}) {
|
|
done := make(chan struct{})
|
|
p.reconfig <- reconfigure{config: config, done: done}
|
|
<-done
|
|
}
|
|
|
|
// Snapshot returns the current status.
|
|
func (p *Plugin) Snapshot() *UpdateRequestV1 {
|
|
ch := make(chan *UpdateRequestV1)
|
|
p.queryCh <- ch
|
|
s := <-ch
|
|
return s
|
|
}
|
|
|
|
// Trigger can be used to control when the plugin attempts to upload
|
|
// status in manual triggering mode.
|
|
func (p *Plugin) Trigger(ctx context.Context) error {
|
|
done := make(chan error)
|
|
p.trigger <- trigger{ctx: ctx, done: done}
|
|
|
|
select {
|
|
case err := <-done:
|
|
return err
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) loop() {
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
for {
|
|
|
|
select {
|
|
case statuses := <-p.pluginStatusCh:
|
|
p.lastPluginStatuses = statuses
|
|
if *p.config.Trigger == plugins.TriggerPeriodic {
|
|
err := p.oneShot(ctx)
|
|
if err != nil {
|
|
p.logger.Error("%v.", err)
|
|
} else {
|
|
p.logger.Info("Status update sent successfully in response to plugin update.")
|
|
}
|
|
}
|
|
|
|
case statuses := <-p.bulkBundleCh:
|
|
p.lastBundleStatuses = statuses
|
|
if *p.config.Trigger == plugins.TriggerPeriodic {
|
|
err := p.oneShot(ctx)
|
|
if err != nil {
|
|
p.logger.Error("%v.", err)
|
|
} else {
|
|
p.logger.Info("Status update sent successfully in response to bundle update.")
|
|
}
|
|
}
|
|
|
|
case status := <-p.bundleCh:
|
|
p.lastBundleStatus = &status
|
|
err := p.oneShot(ctx)
|
|
if err != nil {
|
|
p.logger.Error("%v.", err)
|
|
} else {
|
|
p.logger.Info("Status update sent successfully in response to bundle update.")
|
|
}
|
|
case status := <-p.discoCh:
|
|
p.lastDiscoStatus = &status
|
|
if *p.config.Trigger == plugins.TriggerPeriodic {
|
|
err := p.oneShot(ctx)
|
|
if err != nil {
|
|
p.logger.Error("%v.", err)
|
|
} else {
|
|
p.logger.Info("Status update sent successfully in response to discovery update.")
|
|
}
|
|
}
|
|
case status := <-p.decisionLogsCh:
|
|
p.lastDecisionLogsStatus = &status
|
|
if *p.config.Trigger == plugins.TriggerPeriodic {
|
|
err := p.oneShot(ctx)
|
|
if err != nil {
|
|
p.logger.Error("%v.", err)
|
|
} else {
|
|
p.logger.Info("Status update sent successfully in response to decision log update.")
|
|
}
|
|
}
|
|
case update := <-p.reconfig:
|
|
p.reconfigure(update.config)
|
|
update.done <- struct{}{}
|
|
case respCh := <-p.queryCh:
|
|
respCh <- p.snapshot()
|
|
case update := <-p.trigger:
|
|
err := p.oneShot(update.ctx)
|
|
if err != nil {
|
|
p.logger.Error("%v.", err)
|
|
if update.ctx.Err() == nil {
|
|
update.done <- err
|
|
}
|
|
} else {
|
|
p.logger.Info("Status update sent successfully in response to manual trigger.")
|
|
}
|
|
close(update.done)
|
|
case done := <-p.stop:
|
|
cancel()
|
|
done <- struct{}{}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) oneShot(ctx context.Context) error {
|
|
|
|
req := p.snapshot()
|
|
|
|
if p.config.ConsoleLogs {
|
|
err := p.logUpdate(req)
|
|
if err != nil {
|
|
p.logger.Error("Failed to log to console: %v.", err)
|
|
}
|
|
}
|
|
|
|
if p.config.Prometheus {
|
|
updatePrometheusMetrics(req)
|
|
}
|
|
|
|
if p.config.Plugin != nil {
|
|
proxy, ok := p.manager.Plugin(*p.config.Plugin).(Logger)
|
|
if !ok {
|
|
return fmt.Errorf("plugin does not implement Logger interface")
|
|
}
|
|
return proxy.Log(ctx, req)
|
|
}
|
|
|
|
if p.config.Service != "" {
|
|
resp, err := p.manager.Client(p.config.Service).
|
|
WithJSON(req).
|
|
Do(ctx, "POST", fmt.Sprintf("/status/%v", p.config.PartitionName))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Status update failed: %w", err)
|
|
}
|
|
|
|
defer util.Close(resp)
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("status update failed, server replied with HTTP %v %v", resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) reconfigure(config interface{}) {
|
|
newConfig := config.(*Config)
|
|
|
|
if reflect.DeepEqual(p.config, *newConfig) {
|
|
p.logger.Debug("Status reporter configuration unchanged.")
|
|
return
|
|
}
|
|
|
|
p.logger.Info("Status reporter configuration changed.")
|
|
|
|
if newConfig.Prometheus && !p.config.Prometheus {
|
|
p.registerAll()
|
|
} else if !newConfig.Prometheus && p.config.Prometheus {
|
|
p.unregisterAll()
|
|
}
|
|
|
|
p.config = *newConfig
|
|
}
|
|
|
|
func (p *Plugin) snapshot() *UpdateRequestV1 {
|
|
|
|
s := &UpdateRequestV1{
|
|
Labels: p.manager.Labels(),
|
|
Discovery: p.lastDiscoStatus,
|
|
DecisionLogs: p.lastDecisionLogsStatus,
|
|
Bundle: p.lastBundleStatus,
|
|
Bundles: p.lastBundleStatuses,
|
|
Plugins: p.lastPluginStatuses,
|
|
}
|
|
|
|
if p.metrics != nil {
|
|
s.Metrics = map[string]interface{}{p.metrics.Info().Name: p.metrics.All()}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (p *Plugin) logUpdate(update *UpdateRequestV1) error {
|
|
eventBuf, err := json.Marshal(&update)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fields := map[string]interface{}{}
|
|
err = util.UnmarshalJSON(eventBuf, &fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.manager.ConsoleLogger().WithFields(fields).WithFields(map[string]interface{}{
|
|
"type": "openpolicyagent.org/status",
|
|
}).Info("Status Log")
|
|
return nil
|
|
}
|
|
|
|
func updatePrometheusMetrics(u *UpdateRequestV1) {
|
|
pluginStatus.Reset()
|
|
for name, plugin := range u.Plugins {
|
|
pluginStatus.WithLabelValues(name, string(plugin.State)).Set(1)
|
|
}
|
|
lastSuccessfulActivation.Reset()
|
|
for _, bundle := range u.Bundles {
|
|
if bundle.Code == "" && !bundle.LastSuccessfulActivation.IsZero() {
|
|
loaded.WithLabelValues(bundle.Name).Inc()
|
|
} else {
|
|
failLoad.WithLabelValues(bundle.Name, bundle.Code, bundle.Message).Inc()
|
|
}
|
|
lastSuccessfulActivation.WithLabelValues(bundle.Name, bundle.ActiveRevision).Set(float64(bundle.LastSuccessfulActivation.UnixNano()))
|
|
lastSuccessfulDownload.WithLabelValues(bundle.Name).Set(float64(bundle.LastSuccessfulDownload.UnixNano()))
|
|
lastSuccessfulRequest.WithLabelValues(bundle.Name).Set(float64(bundle.LastSuccessfulRequest.UnixNano()))
|
|
lastRequest.WithLabelValues(bundle.Name).Set(float64(bundle.LastRequest.UnixNano()))
|
|
if bundle.Metrics != nil {
|
|
for stage, metric := range bundle.Metrics.All() {
|
|
switch stage {
|
|
case "timer_bundle_request_ns", "timer_rego_data_parse_ns", "timer_rego_module_parse_ns", "timer_rego_module_compile_ns", "timer_rego_load_bundles_ns":
|
|
bundleLoadDuration.WithLabelValues(bundle.Name, stage).Observe(float64(metric.(int64)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|