diff --git a/CHANGELOG.md b/CHANGELOG.md index 88e59806721d..416839469dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Main (unreleased) - `prometheus.exporter.gcp` - scrape GCP metrics (@tburgessdev) - `otelcol.processor.span` - accepts traces telemetry data from other `otelcol` components and modifies the names and attributes of the spans. (@ptodev) + - `discovery.uyuni` discovers scrape targets from a Uyuni Server. (@sparta0x117) ### Bugfixes diff --git a/component/all/all.go b/component/all/all.go index 8475191ceece..78004969838f 100644 --- a/component/all/all.go +++ b/component/all/all.go @@ -14,6 +14,7 @@ import ( _ "github.com/grafana/agent/component/discovery/kubelet" // Import discovery.kubelet _ "github.com/grafana/agent/component/discovery/kubernetes" // Import discovery.kubernetes _ "github.com/grafana/agent/component/discovery/relabel" // Import discovery.relabel + _ "github.com/grafana/agent/component/discovery/uyuni" // Import discovery.uyuni _ "github.com/grafana/agent/component/local/file" // Import local.file _ "github.com/grafana/agent/component/local/file_match" // Import local.file_match _ "github.com/grafana/agent/component/loki/echo" // Import loki.echo diff --git a/component/discovery/uyuni/uyuni.go b/component/discovery/uyuni/uyuni.go new file mode 100644 index 000000000000..d2b35a824105 --- /dev/null +++ b/component/discovery/uyuni/uyuni.go @@ -0,0 +1,92 @@ +package uyuni + +import ( + "fmt" + "net/url" + "time" + + "github.com/grafana/agent/component" + "github.com/grafana/agent/component/common/config" + "github.com/grafana/agent/component/discovery" + "github.com/grafana/agent/pkg/river/rivertypes" + promcfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + prom_discovery "github.com/prometheus/prometheus/discovery/uyuni" +) + +func init() { + component.Register(component.Registration{ + Name: "discovery.uyuni", + Args: Arguments{}, + Exports: discovery.Exports{}, + + Build: func(opts component.Options, args component.Arguments) (component.Component, error) { + return New(opts, args.(Arguments)) + }, + }) +} + +type Arguments struct { + Server string `river:"server,attr"` + Username string `river:"username,attr"` + Password rivertypes.Secret `river:"password,attr"` + Entitlement string `river:"entitlement,attr,optional"` + Separator string `river:"separator,attr,optional"` + RefreshInterval time.Duration `river:"refresh_interval,attr,optional"` + + ProxyURL config.URL `river:"proxy_url,attr,optional"` + TLSConfig config.TLSConfig `river:"tls_config,block,optional"` + FollowRedirects bool `river:"follow_redirects,attr,optional"` + EnableHTTP2 bool `river:"enable_http2,attr,optional"` +} + +var DefaultArguments = Arguments{ + Entitlement: "monitoring_entitled", + Separator: ",", + RefreshInterval: 1 * time.Minute, + + EnableHTTP2: config.DefaultHTTPClientConfig.EnableHTTP2, + FollowRedirects: config.DefaultHTTPClientConfig.FollowRedirects, +} + +// SetToDefault implements river.Defaulter. +func (a *Arguments) SetToDefault() { + *a = DefaultArguments +} + +// Validate implements river.Validator. +func (a *Arguments) Validate() error { + _, err := url.Parse(a.Server) + if err != nil { + return fmt.Errorf("invalid server URL: %w", err) + } + return a.TLSConfig.Validate() +} + +func (a *Arguments) Convert() *prom_discovery.SDConfig { + return &prom_discovery.SDConfig{ + Server: a.Server, + Username: a.Username, + Password: promcfg.Secret(a.Password), + Entitlement: a.Entitlement, + Separator: a.Separator, + RefreshInterval: model.Duration(a.RefreshInterval), + + HTTPClientConfig: promcfg.HTTPClientConfig{ + ProxyConfig: promcfg.ProxyConfig{ + ProxyURL: a.ProxyURL.Convert(), + }, + TLSConfig: *a.TLSConfig.Convert(), + FollowRedirects: a.FollowRedirects, + EnableHTTP2: a.EnableHTTP2, + }, + } +} + +// New returns a new instance of a discovery.uyuni component. +func New(opts component.Options, args Arguments) (*discovery.Component, error) { + return discovery.New(opts, args, func(args component.Arguments) (discovery.Discoverer, error) { + newArgs := args.(Arguments) + return prom_discovery.NewDiscovery(newArgs.Convert(), opts.Logger) + }) +} diff --git a/component/discovery/uyuni/uyuni_test.go b/component/discovery/uyuni/uyuni_test.go new file mode 100644 index 000000000000..dc018acccaef --- /dev/null +++ b/component/discovery/uyuni/uyuni_test.go @@ -0,0 +1,67 @@ +package uyuni + +import ( + "testing" + "time" + + "github.com/grafana/agent/component/common/config" + "github.com/grafana/agent/pkg/river" + promcfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" +) + +func TestUnmarshal(t *testing.T) { + cfg := ` + server = "https://uyuni.com" + username = "exampleuser" + password = "examplepassword" + refresh_interval = "1m" + tls_config { + ca_file = "/etc/ssl/certs/ca-certificates.crt" + cert_file = "/etc/ssl/certs/client.crt" + key_file = "/etc/ssl/certs/client.key" + } + ` + var args Arguments + err := river.Unmarshal([]byte(cfg), &args) + require.NoError(t, err) +} + +func TestValidate(t *testing.T) { + invalidServer := Arguments{ + Server: "http://uyuni.com", + Username: "exampleuser", + Password: "examplepassword", + TLSConfig: config.TLSConfig{ + CAFile: "/etc/ssl/certs/ca-certificates.crt", + CertFile: "/etc/ssl/certs/client.crt", + + // Check that the TLSConfig is being validated + KeyFile: "/etc/ssl/certs/client.key", + Key: "key", + }, + } + + err := invalidServer.Validate() + require.Error(t, err) +} + +func TestConvert(t *testing.T) { + args := Arguments{ + Server: "https://uyuni.com", + Username: "exampleuser", + Password: "examplepassword", + RefreshInterval: 1 * time.Minute, + EnableHTTP2: true, + FollowRedirects: true, + } + require.NoError(t, args.Validate()) + + converted := args.Convert() + require.Equal(t, "https://uyuni.com", converted.Server) + require.Equal(t, "exampleuser", converted.Username) + require.Equal(t, promcfg.Secret("examplepassword"), converted.Password) + require.Equal(t, model.Duration(1*time.Minute), converted.RefreshInterval) + require.Equal(t, promcfg.DefaultHTTPClientConfig, converted.HTTPClientConfig) +} diff --git a/docs/sources/flow/reference/components/discovery.uyuni.md b/docs/sources/flow/reference/components/discovery.uyuni.md new file mode 100644 index 000000000000..2445729591e4 --- /dev/null +++ b/docs/sources/flow/reference/components/discovery.uyuni.md @@ -0,0 +1,120 @@ +--- +canonical: https://grafana.com/docs/agent/latest/flow/reference/components/discovery.uyuni/ +title: discovery.uyuni +--- + +# discovery.uyuni + +`discovery.uyuni` discovers [Uyuni][] Monitoring Endpoints and exposes them as targets. + +[Uyuni]: https://www.uyuni-project.org/ + +## Usage + +```river +discovery.uyuni "LABEL" { + server = SERVER + username = USERNAME + password = PASSWORD +} +``` + +## Arguments + +The following arguments are supported: + +Name | Type | Description | Default | Required +--------------------- | ---------- | ---------------------------------------------------------------------- | ------------------------ | -------- +`server` | `string` | The primary Uyuni Server. | | yes +`username` | `string` | The username to use for authentication to the Uyuni API. | | yes +`password` | `Secret` | The password to use for authentication to the Uyuni API. | | yes +`entitlement` | `string` | The entitlement to filter on when listing targets. | `"monitoring_entitled"` | no +`separator` | `string` | The separator to use when building the `__meta_uyuni_groups` label. | `","` | no +`refresh_interval` | `duration` | Interval at which to refresh the list of targets. | `1m` | no +`proxy_url` | `string` | HTTP proxy to proxy requests through. | | no +`follow_redirects` | `bool` | Whether redirects returned by the server should be followed. | `true` | no +`enable_http2` | `bool` | Whether HTTP2 is supported for requests. | `true` | no + + +## Blocks +The following blocks are supported inside the definition of +`discovery.uyuni`: + +Hierarchy | Block | Description | Required +--------- | ----- | ----------- | -------- +tls_config | [tls_config][] | TLS configuration for requests to the Uyuni API. | no + +[tls_config]: #tls_config-block + +### tls_config block + +{{< docs/shared lookup="flow/reference/components/tls-config-block.md" source="agent" >}} + +## Exported fields + +The following fields are exported and can be referenced by other components: + +Name | Type | Description +--------- | ------------------- | ----------- +`targets` | `list(map(string))` | The set of targets discovered from the Uyuni API. + +Each target includes the following labels: + +* `__meta_uyuni_minion_hostname`: The hostname of the Uyuni Minion. +* `__meta_uyuni_primary_fqdn`: The FQDN of the Uyuni primary. +* `__meta_uyuni_system_id`: The system ID of the Uyuni Minion. +* `__meta_uyuni_groups`: The groups the Uyuni Minion belongs to. +* `__meta_uyuni_endpoint_name`: The name of the endpoint. +* `__meta_uyuni_exporter`: The name of the exporter. +* `__meta_uyuni_proxy_module`: The name of the Uyuni module. +* `__meta_uyuni_metrics_path`: The path to the metrics endpoint. +* `__meta_uyuni_scheme`: `https` if TLS is enabled on the endpoint, `http` otherwise. + +These labels are largely derived from a [listEndpoints](https://www.uyuni-project.org/uyuni-docs-api/uyuni/api/system.monitoring.html) +API call to the Uyuni Server. + +## Component health + +`discovery.uyuni` is only reported as unhealthy when given an invalid +configuration. In those cases, exported fields retain their last healthy +values. + +## Debug information + +`discovery.uyuni` does not expose any component-specific debug information. + +### Debug metrics + +`discovery.uyuni` does not expose any component-specific debug metrics. + +## Example + +```river +discovery.uyuni "example" { + server = "https://127.0.0.1/rpc/api" + username = UYUNI_USERNAME + password = UYUNI_PASSWORD +} + +prometheus.scrape "demo" { + targets = discovery.uyuni.example.targets + forward_to = [prometheus.remote_write.demo.receiver] +} + +prometheus.remote_write "demo" { + endpoint { + url = PROMETHEUS_REMOTE_WRITE_URL + + basic_auth { + username = USERNAME + password = PASSWORD + } + } +} +``` +Replace the following: + - `UYUNI_USERNAME`: The username to use for authentication to the Uyuni server. + - `UYUNI_PASSWORD`: The password to use for authentication to the Uyuni server. + - `PROMETHEUS_REMOTE_WRITE_URL`: The URL of the Prometheus remote_write-compatible server to send metrics to. + - `USERNAME`: The username to use for authentication to the remote_write API. + - `PASSWORD`: The password to use for authentication to the remote_write API.