• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

opendefensecloud / solution-arsenal / 23050779692

13 Mar 2026 12:26PM UTC coverage: 71.095% (-0.9%) from 72.012%
23050779692

Pull #273

github

web-flow
Merge 418948af2 into b27e7dfd8
Pull Request #273: Target e2e test

5 of 7 new or added lines in 2 files covered. (71.43%)

82 existing lines in 8 files now uncovered.

2098 of 2951 relevant lines covered (71.09%)

18.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.33
/pkg/discovery/handler/handler.go
1
// Copyright 2026 BWI GmbH and Solution Arsenal contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package handler
5

6
import (
7
        "context"
8
        "fmt"
9
        "strings"
10
        "time"
11

12
        "github.com/cenkalti/backoff/v4"
13
        "github.com/go-logr/logr"
14
        "ocm.software/ocm/api/ocm"
15
        "ocm.software/ocm/api/ocm/extensions/repositories/ocireg"
16

17
        "go.opendefense.cloud/solar/pkg/discovery"
18
)
19

20
var (
21
        // handlerRegistry is a map of handler types to their corresponding handlers.
22
        handlerRegistry = make(map[HandlerType]InitHandlerFunc)
23
)
24

25
type InitHandlerFunc func(log logr.Logger) ComponentHandler
26

27
func RegisterComponentHandler(t HandlerType, fn InitHandlerFunc) {
1✔
28
        if fn == nil {
1✔
29
                panic("cannot register nil handler")
×
30
        }
31

32
        if _, exists := handlerRegistry[t]; exists {
1✔
33
                panic(fmt.Sprintf("handler %q already registered", t))
×
34
        }
35

36
        handlerRegistry[t] = fn
1✔
37
}
38

39
type Handler struct {
40
        *discovery.Runner[discovery.ComponentVersionEvent, discovery.WriteAPIResourceEvent]
41
        provider *discovery.RegistryProvider
42
        handler  map[HandlerType]ComponentHandler
43
}
44

45
func NewHandlerOptions(opts ...discovery.RunnerOption[discovery.ComponentVersionEvent, discovery.WriteAPIResourceEvent]) []discovery.RunnerOption[discovery.ComponentVersionEvent, discovery.WriteAPIResourceEvent] {
1✔
46
        return opts
1✔
47
}
1✔
48

49
func NewHandler(
50
        provider *discovery.RegistryProvider,
51
        in <-chan discovery.ComponentVersionEvent,
52
        out chan<- discovery.WriteAPIResourceEvent,
53
        err chan<- discovery.ErrorEvent,
54
        opts ...discovery.RunnerOption[discovery.ComponentVersionEvent, discovery.WriteAPIResourceEvent],
55
) *Handler {
3✔
56
        p := &Handler{
3✔
57
                provider: provider,
3✔
58
                handler:  make(map[HandlerType]ComponentHandler),
3✔
59
        }
3✔
60
        p.Runner = discovery.NewRunner(p, in, out, err)
3✔
61
        for _, opt := range opts {
6✔
62
                opt(p.Runner)
3✔
63
        }
3✔
64

65
        return p
3✔
66
}
67

68
// isRetryable determines if we should wait and try again
69
func isRetryable(err error) bool {
×
70
        msg := strings.ToLower(err.Error())
×
71
        // OCM often wraps errors, so we check the string for common rate-limit indicators
×
72
        return strings.Contains(msg, "429") ||
×
73
                strings.Contains(msg, "too many requests") ||
×
74
                strings.Contains(msg, "connection refused")
×
75
}
×
76

77
func (rs *Handler) Process(ctx context.Context, ev discovery.ComponentVersionEvent) ([]discovery.WriteAPIResourceEvent, error) {
2✔
78
        rs.Logger().Info("processing component version event", "event", ev)
2✔
79
        comp := ev.Component
2✔
80
        version := ev.Source.Version
2✔
81

2✔
82
        // Analyze resources contained in component descriptor.
2✔
83
        helmChartCount := 0
2✔
84
        handlerType := HandlerType("")
2✔
85

2✔
86
        // Exit early on deletion
2✔
87
        if ev.Source.Type == discovery.EventDeleted {
2✔
88
                return []discovery.WriteAPIResourceEvent{{
×
89
                        Source:    ev,
×
90
                        Timestamp: time.Now().UTC(),
×
91
                }}, nil
×
92
        }
×
93

94
        // Get registry configuration
95
        registry := rs.provider.Get(ev.Source.Registry)
2✔
96
        if registry == nil {
2✔
97
                rs.Logger().V(2).Info("invalid registry", "registry", ev.Source.Registry)
×
98
                return nil, fmt.Errorf("invalid registry: %s", ev.Source.Registry)
×
99
        }
×
100

101
        var octx ocm.Context
2✔
102
        var err error
2✔
103
        if registry.Credentials != nil {
2✔
UNCOV
104
                octx, err = discovery.FromContextWithCreds(ctx, registry)
×
UNCOV
105
                if err != nil {
×
106
                        return nil, fmt.Errorf("failed to create OCM context with creds: %w", err)
×
107
                }
×
108
        } else {
2✔
109
                octx = ocm.FromContext(ctx)
2✔
110
        }
2✔
111

112
        // Create repository for the component
113
        baseURL := fmt.Sprintf("%s/%s", registry.GetURL(), ev.Namespace)
2✔
114
        repo, err := octx.RepositoryForSpec(ocireg.NewRepositorySpec(baseURL))
2✔
115
        if err != nil {
2✔
116
                rs.Logger().Error(err, "failed to create repo spec", "registry", ev.Source.Registry, "repository", ev.Source.Repository)
×
117
                return nil, fmt.Errorf("failed to create repository spec: %w", err)
×
118
        }
×
119
        defer func() { _ = repo.Close() }()
4✔
120

121
        // Lookup the specific component version
122
        var compVersion ocm.ComponentVersionAccess
2✔
123
        if rs.Backoff() == nil {
4✔
124
                compVersion, err = repo.LookupComponentVersion(comp, version)
2✔
125
        } else {
2✔
126
                // If backoff is configured, use it to retry on transient errors
×
UNCOV
127
                operation := func() error {
×
UNCOV
128
                        var err error
×
129
                        compVersion, err = repo.LookupComponentVersion(comp, version)
×
UNCOV
130
                        if err != nil {
×
131
                                // Check if the error is a 429 or transient
×
UNCOV
132
                                if isRetryable(err) {
×
UNCOV
133
                                        return err // Returning error triggers a retry
×
134
                                }
×
135

136
                                return backoff.Permanent(err) // Stops retrying for 401, 404, etc.
×
137
                        }
138

UNCOV
139
                        return nil
×
140
                }
UNCOV
141
                err = backoff.Retry(operation, rs.Backoff())
×
142
        }
143
        if err != nil {
2✔
UNCOV
144
                rs.Logger().Error(err, "failed to lookup component", "version", version)
×
UNCOV
145
                return nil, fmt.Errorf("failed to lookup component version %s: %w", version, err)
×
UNCOV
146
        }
×
147
        defer func() { _ = compVersion.Close() }()
4✔
148

149
        // Count the number of Helm chart resources in the component version and determine the handler type based on that.
150
        for _, res := range compVersion.GetDescriptor().ComponentSpec.Resources {
12✔
151
                if res.Type == string(HelmResource) {
12✔
152
                        helmChartCount++
2✔
153
                }
2✔
154
        }
155

156
        // Classify component based on contained resources as helm chart and send it to the corresponding handler.
157
        if helmChartCount == 1 {
4✔
158
                handlerType = HelmHandler
2✔
159
        }
2✔
160

161
        // If no handler type could be determined, log and publish error.
162
        if handlerType == "" {
2✔
163
                // No handler found for event, log and publish error.
×
UNCOV
164
                rs.Logger().Info("no handler found for event", "event", ev)
×
UNCOV
165
                return nil, fmt.Errorf("no handler found for component version event: %v", ev)
×
UNCOV
166
        }
×
167

168
        rs.Logger().Info("info", "compVersion", compVersion)
2✔
169

2✔
170
        // Process component with determined handler type.
2✔
171
        h, err := rs.getHandler(handlerType)
2✔
172
        if err != nil {
2✔
UNCOV
173
                rs.Logger().Error(err, "failed to process component with handler", "handler", handlerType)
×
UNCOV
174
                return nil, fmt.Errorf("failed to process component with handler %q: %w", handlerType, err)
×
UNCOV
175
        }
×
176

177
        // Process component with determined handler. If processing fails, log and publish error.
178
        resEvent, err := h.Process(octx, &ev, compVersion)
2✔
179
        if err != nil {
2✔
UNCOV
180
                rs.Logger().Error(err, "failed to process component with handler", "handler", handlerType)
×
UNCOV
181
                return nil, fmt.Errorf("failed to process component with handler %q: %w", handlerType, err)
×
UNCOV
182
        }
×
183

184
        return []discovery.WriteAPIResourceEvent{*resEvent}, nil
2✔
185
}
186

187
// getHandler returns the handler for the given type, initializing it if necessary.
188
func (rs *Handler) getHandler(t HandlerType) (ComponentHandler, error) {
2✔
189
        if rs.handler[HelmHandler] == nil {
4✔
190
                if initFn, ok := handlerRegistry[HelmHandler]; ok {
4✔
191
                        handler := initFn(rs.Logger().WithValues("handler", HelmHandler))
2✔
192
                        rs.handler[HelmHandler] = handler
2✔
193

2✔
194
                        return handler, nil
2✔
195
                }
2✔
196
        }
197

UNCOV
198
        return nil, fmt.Errorf("no handler registered for type %v", t)
×
199
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc