diff --git a/internal/component/registry.go b/internal/component/registry.go index a7c0aca910..b382719bcd 100644 --- a/internal/component/registry.go +++ b/internal/component/registry.go @@ -124,7 +124,7 @@ type Registration struct { // sure the user is not accidentally using a component that is not yet stable - users // need to explicitly enable less-than-stable components via, for example, a command-line flag. // If a component is not stable enough, an attempt to create it via the controller will fail. - // The default stability level is Experimental. + // This field must be set to a non-zero value. Stability featuregate.Stability // An example Arguments value that the registered component expects to diff --git a/internal/flow/flow_services_test.go b/internal/flow/flow_services_test.go index 86e375132f..80404b80f4 100644 --- a/internal/flow/flow_services_test.go +++ b/internal/flow/flow_services_test.go @@ -63,6 +63,7 @@ func TestServices_Configurable(t *testing.T) { return service.Definition{ Name: "fake", ConfigType: ServiceOptions{}, + Stability: featuregate.StabilityBeta, } }, @@ -117,6 +118,7 @@ func TestServices_Configurable_Optional(t *testing.T) { return service.Definition{ Name: "fake", ConfigType: ServiceOptions{}, + Stability: featuregate.StabilityBeta, } }, diff --git a/internal/flow/internal/controller/loader.go b/internal/flow/internal/controller/loader.go index 43e102963e..8921d5ff18 100644 --- a/internal/flow/internal/controller/loader.go +++ b/internal/flow/internal/controller/loader.go @@ -10,6 +10,7 @@ import ( "time" "github.com/go-kit/log" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/flow/internal/dag" "github.com/grafana/agent/internal/flow/internal/worker" "github.com/grafana/agent/internal/flow/logging/level" @@ -441,6 +442,19 @@ func (l *Loader) populateServiceNodes(g *dag.Graph, serviceBlocks []*ast.BlockSt node := g.GetByID(blockID).(*ServiceNode) + // Don't permit configuring services that have a lower stability level than + // what is currently enabled. + nodeStability := node.Service().Definition().Stability + if err := featuregate.CheckAllowed(nodeStability, l.globals.MinStability, fmt.Sprintf("block %q", blockID)); err != nil { + diags.Add(diag.Diagnostic{ + Severity: diag.SeverityLevelError, + Message: err.Error(), + StartPos: ast.StartPos(block).Position(), + EndPos: ast.EndPos(block).Position(), + }) + continue + } + // Blocks assigned to services are reset to nil in the previous loop. // // If the block is non-nil, it means that there was a duplicate block diff --git a/internal/flow/internal/controller/loader_test.go b/internal/flow/internal/controller/loader_test.go index 672e2dce67..398cd5cae0 100644 --- a/internal/flow/internal/controller/loader_test.go +++ b/internal/flow/internal/controller/loader_test.go @@ -1,6 +1,7 @@ package controller_test import ( + "context" "errors" "os" "strings" @@ -11,6 +12,7 @@ import ( "github.com/grafana/agent/internal/flow/internal/controller" "github.com/grafana/agent/internal/flow/internal/dag" "github.com/grafana/agent/internal/flow/logging" + "github.com/grafana/agent/internal/service" "github.com/grafana/river/ast" "github.com/grafana/river/diag" "github.com/grafana/river/parser" @@ -316,6 +318,60 @@ func TestLoader(t *testing.T) { }) } +func TestLoader_Services(t *testing.T) { + testFile := ` + testsvc { } + ` + + testService := &fakeService{ + DefinitionFunc: func() service.Definition { + return service.Definition{ + Name: "testsvc", + ConfigType: struct { + Name string `river:"name,attr,optional"` + }{}, + Stability: featuregate.StabilityBeta, + } + }, + } + + newLoaderOptionsWithStability := func(stability featuregate.Stability) controller.LoaderOptions { + l, _ := logging.New(os.Stderr, logging.DefaultOptions) + return controller.LoaderOptions{ + ComponentGlobals: controller.ComponentGlobals{ + Logger: l, + TraceProvider: noop.NewTracerProvider(), + DataPath: t.TempDir(), + MinStability: stability, + OnBlockNodeUpdate: func(cn controller.BlockNode) { /* no-op */ }, + Registerer: prometheus.NewRegistry(), + NewModuleController: func(id string) controller.ModuleController { + return nil + }, + }, + Services: []service.Service{testService}, + } + } + + t.Run("Load with service at correct stability level", func(t *testing.T) { + l := controller.NewLoader(newLoaderOptionsWithStability(featuregate.StabilityBeta)) + diags := applyFromContent(t, l, []byte(testFile), nil, nil) + require.NoError(t, diags.ErrorOrNil()) + }) + + t.Run("Load with service below minimum stabilty level", func(t *testing.T) { + l := controller.NewLoader(newLoaderOptionsWithStability(featuregate.StabilityStable)) + diags := applyFromContent(t, l, []byte(testFile), nil, nil) + require.ErrorContains(t, diags.ErrorOrNil(), `block "testsvc" is at stability level "beta", which is below the minimum allowed stability level "stable"`) + }) + + t.Run("Load with undefined minimum stability level", func(t *testing.T) { + l := controller.NewLoader(newLoaderOptionsWithStability(featuregate.StabilityUndefined)) + diags := applyFromContent(t, l, []byte(testFile), nil, nil) + require.ErrorContains(t, diags.ErrorOrNil(), `stability levels must be defined: got "beta" as stability of block "testsvc" and as the minimum stability level`) + }) +} + // TestScopeWithFailingComponent is used to ensure that the scope is filled out, even if the component // fails to properly start. func TestScopeWithFailingComponent(t *testing.T) { @@ -473,3 +529,37 @@ func (f fakeModuleController) ClearModuleIDs() { func (f fakeModuleController) NewCustomComponent(id string, export component.ExportFunc) (controller.CustomComponent, error) { return nil, nil } + +type fakeService struct { + DefinitionFunc func() service.Definition // Required. + RunFunc func(ctx context.Context, host service.Host) error + UpdateFunc func(newConfig any) error + DataFunc func() any +} + +func (fs *fakeService) Definition() service.Definition { + return fs.DefinitionFunc() +} + +func (fs *fakeService) Run(ctx context.Context, host service.Host) error { + if fs.RunFunc != nil { + return fs.RunFunc(ctx, host) + } + + <-ctx.Done() + return nil +} + +func (fs *fakeService) Update(newConfig any) error { + if fs.UpdateFunc != nil { + return fs.UpdateFunc(newConfig) + } + return nil +} + +func (fs *fakeService) Data() any { + if fs.DataFunc != nil { + return fs.DataFunc() + } + return nil +} diff --git a/internal/service/cluster/cluster.go b/internal/service/cluster/cluster.go index 6c404b65f5..30c7a1cc45 100644 --- a/internal/service/cluster/cluster.go +++ b/internal/service/cluster/cluster.go @@ -15,6 +15,7 @@ import ( "github.com/go-kit/log" "github.com/grafana/agent/internal/component" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/flow/logging/level" "github.com/grafana/agent/internal/service" http_service "github.com/grafana/agent/internal/service/http" @@ -162,6 +163,7 @@ func (s *Service) Definition() service.Definition { // Cluster depends on the HTTP service to work properly. http_service.ServiceName, }, + Stability: featuregate.StabilityStable, } } diff --git a/internal/service/http/http.go b/internal/service/http/http.go index 16e9d8449f..26c93e3911 100644 --- a/internal/service/http/http.go +++ b/internal/service/http/http.go @@ -16,6 +16,7 @@ import ( "github.com/go-kit/log" "github.com/gorilla/mux" "github.com/grafana/agent/internal/component" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/flow" "github.com/grafana/agent/internal/flow/logging/level" "github.com/grafana/agent/internal/service" @@ -128,6 +129,7 @@ func (s *Service) Definition() service.Definition { Name: ServiceName, ConfigType: Arguments{}, DependsOn: nil, // http has no dependencies. + Stability: featuregate.StabilityStable, } } diff --git a/internal/service/labelstore/service.go b/internal/service/labelstore/service.go index 6906462606..3a536ff2dc 100644 --- a/internal/service/labelstore/service.go +++ b/internal/service/labelstore/service.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-kit/log" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/flow/logging/level" agent_service "github.com/grafana/agent/internal/service" flow_service "github.com/grafana/agent/internal/service" @@ -67,6 +68,7 @@ func (s *service) Definition() agent_service.Definition { Name: ServiceName, ConfigType: Arguments{}, DependsOn: nil, + Stability: featuregate.StabilityStable, } } diff --git a/internal/service/otel/otel.go b/internal/service/otel/otel.go index f8e4707250..4713abaeaf 100644 --- a/internal/service/otel/otel.go +++ b/internal/service/otel/otel.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/go-kit/log" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/service" "github.com/grafana/agent/internal/util" ) @@ -50,6 +51,7 @@ func (*Service) Definition() service.Definition { Name: ServiceName, ConfigType: nil, // otel does not accept configuration DependsOn: []string{}, + Stability: featuregate.StabilityStable, } } diff --git a/internal/service/remotecfg/remotecfg.go b/internal/service/remotecfg/remotecfg.go index 318404227d..6ddad9b824 100644 --- a/internal/service/remotecfg/remotecfg.go +++ b/internal/service/remotecfg/remotecfg.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/agent-remote-config/api/gen/proto/go/agent/v1/agentv1connect" "github.com/grafana/agent/internal/agentseed" "github.com/grafana/agent/internal/component/common/config" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/flow/logging/level" "github.com/grafana/agent/internal/service" "github.com/grafana/river" @@ -128,6 +129,7 @@ func (s *Service) Definition() service.Definition { Name: ServiceName, ConfigType: Arguments{}, DependsOn: nil, // remotecfg has no dependencies. + Stability: featuregate.StabilityBeta, } } diff --git a/internal/service/service.go b/internal/service/service.go index 344ff11341..62751d464c 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -11,6 +11,7 @@ import ( "fmt" "github.com/grafana/agent/internal/component" + "github.com/grafana/agent/internal/featuregate" ) // Definition describes an individual Flow service. Services have unique names @@ -35,6 +36,14 @@ type Definition struct { // or a named service doesn't exist), it is treated as a fatal // error and the root Flow module will exit. DependsOn []string + + // Stability is the overall stability level of the service. This is used to + // make sure the user is not accidentally configuring a service that is not + // yet stable - users need to explicitly enable less-than-stable services + // via, for example, a command-line flag. If a service is not stable enough, + // an attempt to configure it via the controller will fail. + // This field must be set to a non-zero value. + Stability featuregate.Stability } // Host is a controller for services and Flow components. diff --git a/internal/service/ui/ui.go b/internal/service/ui/ui.go index bbf62b748b..ebc56943f8 100644 --- a/internal/service/ui/ui.go +++ b/internal/service/ui/ui.go @@ -8,6 +8,7 @@ import ( "path" "github.com/gorilla/mux" + "github.com/grafana/agent/internal/featuregate" "github.com/grafana/agent/internal/service" http_service "github.com/grafana/agent/internal/service/http" "github.com/grafana/agent/internal/web/api" @@ -46,6 +47,7 @@ func (s *Service) Definition() service.Definition { Name: ServiceName, ConfigType: nil, // ui does not accept configuration DependsOn: []string{http_service.ServiceName}, + Stability: featuregate.StabilityStable, } }