Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkg/operator: refactor resource hierarchy discovery #1271

Merged
merged 7 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/operator/apis/monitoring/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
prom_v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// +kubebuilder:object:root=true
Expand All @@ -24,6 +25,7 @@ type GrafanaAgent struct {
// MetricsInstanceSelector returns a selector to find MetricsInstances.
func (a *GrafanaAgent) MetricsInstanceSelector() ObjectSelector {
return ObjectSelector{
ObjectType: &MetricsInstance{},
ParentNamespace: a.Namespace,
NamespaceSelector: a.Spec.Metrics.InstanceNamespaceSelector,
Labels: a.Spec.Metrics.InstanceSelector,
Expand All @@ -33,6 +35,7 @@ func (a *GrafanaAgent) MetricsInstanceSelector() ObjectSelector {
// LogsInstanceSelector returns a selector to find LogsInstances.
func (a *GrafanaAgent) LogsInstanceSelector() ObjectSelector {
return ObjectSelector{
ObjectType: &LogsInstance{},
ParentNamespace: a.Namespace,
NamespaceSelector: a.Spec.Logs.InstanceNamespaceSelector,
Labels: a.Spec.Logs.InstanceSelector,
Expand Down Expand Up @@ -153,6 +156,7 @@ type GrafanaAgentSpec struct {
// resource hierarchy. When NamespaceSelector is nil, objects should be
// searched directly in the ParentNamespace.
type ObjectSelector struct {
ObjectType client.Object
ParentNamespace string
NamespaceSelector *metav1.LabelSelector
Labels *metav1.LabelSelector
Expand Down
5 changes: 3 additions & 2 deletions pkg/operator/apis/monitoring/v1alpha1/types_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ type LogsInstance struct {
Spec LogsInstanceSpec `json:"spec,omitempty"`
}

// PodLogsInstanceSelector returns the selector to discover PodLogs.
func (i *LogsInstance) PodLogsInstanceSelector() ObjectSelector {
// PodLogsSelector returns the selector to discover PodLogs.
func (i *LogsInstance) PodLogsSelector() ObjectSelector {
return ObjectSelector{
ObjectType: &PodLogs{},
ParentNamespace: i.Namespace,
NamespaceSelector: i.Spec.PodLogsNamespaceSelector,
Labels: i.Spec.PodLogsSelector,
Expand Down
3 changes: 3 additions & 0 deletions pkg/operator/apis/monitoring/v1alpha1/types_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ type MetricsInstance struct {
// ServiceMonitorSelector returns a selector to find ServiceMonitors.
func (p *MetricsInstance) ServiceMonitorSelector() ObjectSelector {
return ObjectSelector{
ObjectType: &prom_v1.ServiceMonitor{},
ParentNamespace: p.Namespace,
NamespaceSelector: p.Spec.ServiceMonitorNamespaceSelector,
Labels: p.Spec.ServiceMonitorSelector,
Expand All @@ -191,6 +192,7 @@ func (p *MetricsInstance) ServiceMonitorSelector() ObjectSelector {
// PodMonitorSelector returns a selector to find PodMonitors.
func (p *MetricsInstance) PodMonitorSelector() ObjectSelector {
return ObjectSelector{
ObjectType: &prom_v1.PodMonitor{},
ParentNamespace: p.Namespace,
NamespaceSelector: p.Spec.PodMonitorNamespaceSelector,
Labels: p.Spec.PodMonitorSelector,
Expand All @@ -200,6 +202,7 @@ func (p *MetricsInstance) PodMonitorSelector() ObjectSelector {
// ProbeSelector returns a selector to find Probes.
func (p *MetricsInstance) ProbeSelector() ObjectSelector {
return ObjectSelector{
ObjectType: &prom_v1.Probe{},
ParentNamespace: p.Namespace,
NamespaceSelector: p.Spec.ProbeNamespaceSelector,
Labels: p.Spec.ProbeSelector,
Expand Down
282 changes: 282 additions & 0 deletions pkg/operator/build_hierarchy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package operator

import (
"context"
"fmt"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
grafana "github.com/grafana/agent/pkg/operator/apis/monitoring/v1alpha1"
"github.com/grafana/agent/pkg/operator/assets"
"github.com/grafana/agent/pkg/operator/config"
"github.com/grafana/agent/pkg/operator/hierarchy"
prom "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

// buildHierarchy constructs a resource hierarchy starting from root.
func buildHierarchy(ctx context.Context, l log.Logger, cli client.Client, root *grafana.GrafanaAgent) (deployment config.Deployment, watchers []hierarchy.Watcher, err error) {
deployment.Agent = root

// search is used throughout BuildHierarchy, where it will perform a list for
// a set of objects in the hierarchy and populate the watchers return
// variable.
search := func(resources []hierarchyResource) error {
for _, res := range resources {
sel, err := res.Find(ctx, cli)
if err != nil {
gvk, _ := apiutil.GVKForObject(res.List, cli.Scheme())
return fmt.Errorf("failed to find %q resource: %w", gvk.String(), err)
}

watchers = append(watchers, hierarchy.Watcher{
Object: res.Selector.ObjectType,
Owner: client.ObjectKeyFromObject(root),
Selector: sel,
})
}
return nil
}

// Root resources
var (
metricInstances grafana.MetricsInstanceList
logsInstances grafana.LogsInstanceList
)
var roots = []hierarchyResource{
{List: &metricInstances, Selector: root.MetricsInstanceSelector()},
{List: &logsInstances, Selector: root.LogsInstanceSelector()},
}
if err := search(roots); err != nil {
return deployment, nil, err
}

// Metrics resources
for _, metricsInst := range metricInstances.Items {
var (
serviceMonitors prom.ServiceMonitorList
podMonitors prom.PodMonitorList
probes prom.ProbeList
)
var children = []hierarchyResource{
{List: &serviceMonitors, Selector: metricsInst.ServiceMonitorSelector()},
{List: &podMonitors, Selector: metricsInst.PodMonitorSelector()},
{List: &probes, Selector: metricsInst.ProbeSelector()},
}
if err := search(children); err != nil {
return deployment, nil, err
}

deployment.Metrics = append(deployment.Metrics, config.MetricsInstance{
Instance: metricsInst,
ServiceMonitors: filterServiceMonitors(l, root, &serviceMonitors).Items,
PodMonitors: podMonitors.Items,
Probes: probes.Items,
})
}

// Logs resources
for _, logsInst := range logsInstances.Items {
var (
podLogs grafana.PodLogsList
)
var children = []hierarchyResource{
{List: &podLogs, Selector: logsInst.PodLogsSelector()},
}
if err := search(children); err != nil {
return deployment, nil, err
}

deployment.Logs = append(deployment.Logs, config.LogInstance{
Instance: logsInst,
PodLogs: podLogs.Items,
})
}

// Finally, find all referenced secrets
secrets, secretWatchers, err := buildSecrets(ctx, cli, deployment)
if err != nil {
return deployment, nil, fmt.Errorf("failed to discover secrets: %w", err)
}
deployment.Secrets = secrets
watchers = append(watchers, secretWatchers...)

return deployment, watchers, nil
}

type hierarchyResource struct {
List client.ObjectList // List to populate
Selector grafana.ObjectSelector // Raw selector to use for list
}

func (hr *hierarchyResource) Find(ctx context.Context, cli client.Client) (hierarchy.Selector, error) {
sel, err := toSelector(hr.Selector)
if err != nil {
return nil, fmt.Errorf("failed to build selector: %w", err)
}
err = hierarchy.List(ctx, cli, hr.List, sel)
if err != nil {
return nil, fmt.Errorf("failed to list resources: %w", err)
}
return sel, nil
}

func toSelector(os grafana.ObjectSelector) (hierarchy.Selector, error) {
var res hierarchy.LabelsSelector
res.NamespaceName = os.ParentNamespace

if os.NamespaceSelector != nil {
sel, err := metav1.LabelSelectorAsSelector(os.NamespaceSelector)
if err != nil {
return nil, fmt.Errorf("invalid namespace selector: %w", err)
}
res.NamespaceLabels = sel
}

sel, err := metav1.LabelSelectorAsSelector(os.Labels)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %w", err)
}
res.Labels = sel
return &res, nil
}

func filterServiceMonitors(l log.Logger, root *grafana.GrafanaAgent, list *prom.ServiceMonitorList) *prom.ServiceMonitorList {
items := make([]*prom.ServiceMonitor, 0, len(list.Items))

Item:
for _, item := range list.Items {
if root.Spec.Metrics.ArbitraryFSAccessThroughSMs.Deny {
for _, ep := range item.Spec.Endpoints {
err := testForArbitraryFSAccess(ep)
if err == nil {
continue
}

level.Warn(l).Log(
"msg", "skipping service monitor",
"agent", client.ObjectKeyFromObject(root),
"servicemonitor", client.ObjectKeyFromObject(item),
"err", err,
)
continue Item
}
}
items = append(items, item)
}

return &prom.ServiceMonitorList{
TypeMeta: list.TypeMeta,
ListMeta: *list.ListMeta.DeepCopy(),
Items: items,
}
}

func testForArbitraryFSAccess(e prom.Endpoint) error {
if e.BearerTokenFile != "" {
return fmt.Errorf("it accesses file system via bearer token file which is disallowed via GrafanaAgent specification")
}

if e.TLSConfig == nil {
return nil
}

if e.TLSConfig.CAFile != "" || e.TLSConfig.CertFile != "" || e.TLSConfig.KeyFile != "" {
return fmt.Errorf("it accesses file system via TLS config which is disallowed via GrafanaAgent specification")
}

return nil
}

func buildSecrets(ctx context.Context, cli client.Client, deploy config.Deployment) (secrets assets.SecretStore, watchers []hierarchy.Watcher, err error) {
secrets = make(assets.SecretStore)

// KeySelector caches to make sure we don't create duplicate watchers.
var (
usedSecretSelectors = map[hierarchy.KeySelector]struct{}{}
usedConfigMapSelectors = map[hierarchy.KeySelector]struct{}{}
)

for _, ref := range deploy.AssetReferences() {
var (
objectList client.ObjectList
sel hierarchy.KeySelector
)

switch {
case ref.Reference.Secret != nil:
objectList = &corev1.SecretList{}
sel = hierarchy.KeySelector{
Namespace: ref.Namespace,
Name: ref.Reference.Secret.Name,
}
case ref.Reference.ConfigMap != nil:
objectList = &corev1.ConfigMapList{}
sel = hierarchy.KeySelector{
Namespace: ref.Namespace,
Name: ref.Reference.ConfigMap.Name,
}
}

gvk, _ := apiutil.GVKForObject(objectList, cli.Scheme())
if err := hierarchy.List(ctx, cli, objectList, &sel); err != nil {
return nil, nil, fmt.Errorf("failed to find %q resource: %w", gvk.String(), err)
}

err := meta.EachListItem(objectList, func(o runtime.Object) error {
var value string

switch o := o.(type) {
case *corev1.Secret:
rawValue, ok := o.Data[ref.Reference.Secret.Key]
if !ok {
return fmt.Errorf("no key %s in Secret %s", ref.Reference.ConfigMap.Key, o.Name)
}
value = string(rawValue)
case *corev1.ConfigMap:
rawValue, ok := o.BinaryData[ref.Reference.ConfigMap.Key]
if !ok {
return fmt.Errorf("no key %s in ConfigMap %s", ref.Reference.ConfigMap.Key, o.Name)
}
value = string(rawValue)
}

secrets[assets.KeyForSelector(ref.Namespace, &ref.Reference)] = value
return nil
})
if err != nil {
return nil, nil, fmt.Errorf("failed to iterate over %q list: %w", gvk.String(), err)
}

switch {
case ref.Reference.Secret != nil:
if _, used := usedSecretSelectors[sel]; used {
continue
}
watchers = append(watchers, hierarchy.Watcher{
Object: &v1.Secret{},
Owner: client.ObjectKeyFromObject(deploy.Agent),
Selector: &sel,
})
usedSecretSelectors[sel] = struct{}{}
case ref.Reference.ConfigMap != nil:
if _, used := usedConfigMapSelectors[sel]; used {
continue
}
watchers = append(watchers, hierarchy.Watcher{
Object: &v1.ConfigMap{},
Owner: client.ObjectKeyFromObject(deploy.Agent),
Selector: &sel,
})
usedConfigMapSelectors[sel] = struct{}{}
}
}

return secrets, watchers, nil
}
Loading