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

opendefensecloud / solution-arsenal / 25550063163

08 May 2026 10:15AM UTC coverage: 70.957% (+0.03%) from 70.931%
25550063163

Pull #515

github

web-flow
Merge 2643a0ef2 into 36ef0d504
Pull Request #515: chore: consolodate registry types

47 of 74 new or added lines in 9 files covered. (63.51%)

10 existing lines in 2 files now uncovered.

2321 of 3271 relevant lines covered (70.96%)

21.79 hits per line

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

60.48
/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 {
4✔
56
        p := &Handler{
4✔
57
                provider: provider,
4✔
58
                handler:  make(map[HandlerType]ComponentHandler),
4✔
59
        }
4✔
60
        p.Runner = discovery.NewRunner(p, in, out, err)
4✔
61
        for _, opt := range opts {
8✔
62
                opt(p.Runner)
4✔
63
        }
4✔
64

65
        return p
4✔
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
        creds := rs.provider.GetCredentials(ev.Source.Registry)
2✔
104
        if creds != nil {
2✔
NEW
105
                octx, err = discovery.FromContextWithCreds(ctx, registry.Spec.Hostname, creds)
×
106
                if err != nil {
×
107
                        return nil, fmt.Errorf("failed to create OCM context with creds: %w", err)
×
108
                }
×
109
        } else {
2✔
110
                octx = ocm.FromContext(ctx)
2✔
111
        }
2✔
112

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

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

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

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

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

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

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

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

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

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

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

188
// getHandlerForType returns the handler for the given type, initializing it if necessary.
189
func (rs *Handler) getHandlerForType(t HandlerType) (ComponentHandler, error) {
5✔
190
        if h, ok := rs.handler[t]; ok {
6✔
191

1✔
192
                return h, nil
1✔
193
        }
1✔
194

195
        if initFn, ok := handlerRegistry[t]; ok {
7✔
196
                h := initFn(rs.Logger().WithValues("handler", t))
3✔
197
                rs.handler[t] = h
3✔
198

3✔
199
                return h, nil
3✔
200
        }
3✔
201

202
        return nil, fmt.Errorf("no handler registered for type %v", t)
1✔
203
}
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