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

bakito / sealed-secrets-web / 17078808073

19 Aug 2025 06:44PM UTC coverage: 70.562% (+8.6%) from 62.007%
17078808073

push

github

web-flow
feat: add regex include and exclude for namespaces (#323)

95 of 126 new or added lines in 2 files covered. (75.4%)

2 existing lines in 1 file now uncovered.

477 of 676 relevant lines covered (70.56%)

0.79 hits per line

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

57.02
/pkg/handler/secrets.go
1
package handler
2

3
import (
4
        "context"
5
        "fmt"
6
        "log"
7
        "net/http"
8
        "sort"
9
        "strings"
10

11
        "github.com/bakito/sealed-secrets-web/pkg/config"
12
        ssClient "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1"
13
        "github.com/gin-gonic/gin"
14
        v1 "k8s.io/api/core/v1"
15
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
        "k8s.io/apimachinery/pkg/runtime"
17
        "k8s.io/apimachinery/pkg/runtime/schema"
18
        "k8s.io/client-go/kubernetes/scheme"
19
        corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
20
        "k8s.io/client-go/tools/clientcmd"
21
)
22

23
// BuildClients builds the Kubernetes clients
24
// This function creates two clients: one for standard Kubernetes resources and one for Sealed Secrets
25
func BuildClients(
26
        clientConfig clientcmd.ClientConfig, // Configuration for the Kubernetes connection
27
        disableLoadSecrets bool,             // Flag to disable loading secrets
28
) (corev1.CoreV1Interface, ssClient.BitnamiV1alpha1Interface, error) {
×
NEW
29
        // If loading secrets is disabled, return empty clients
×
30
        if disableLoadSecrets {
×
31
                return nil, nil, nil
×
32
        }
×
33

34
        // Create client configuration from the provided config
35
        conf, err := clientConfig.ClientConfig()
×
36
        if err != nil {
×
37
                return nil, nil, err
×
38
        }
×
39

40
        // Create standard Kubernetes client for core resources (including Secrets)
41
        restClient, err := corev1.NewForConfig(conf)
×
42
        if err != nil {
×
43
                return nil, nil, err
×
44
        }
×
45

46
        // Create specialized client for Sealed Secrets
47
        ssCl, err := ssClient.NewForConfig(conf)
×
48
        if err != nil {
×
49
                return nil, nil, err
×
50
        }
×
51

52
        return restClient, ssCl, nil
×
53
}
54

55
// SecretsHandler manages all operations for secrets
56
type SecretsHandler struct {
57
        coreClient         corev1.CoreV1Interface            // Client for standard Kubernetes resources
58
        ssClient           ssClient.BitnamiV1alpha1Interface // Client for Sealed Secrets
59
        disableLoadSecrets bool                              // Flag whether secrets can be loaded
60
        includeNamespaces  map[string]bool                   // Map for quick checking if a namespace is included
61
        config             *config.Config                    // General configuration
62
}
63

64
// NewHandler creates a new secrets handler
65
func NewHandler(
66
        coreClient corev1.CoreV1Interface,
67
        ssCl ssClient.BitnamiV1alpha1Interface,
68
        cfg *config.Config,
69
) *SecretsHandler {
1✔
70
        // Create a map for quick lookups of included namespaces
1✔
71
        inMap := make(map[string]bool)
1✔
72
        for _, n := range cfg.IncludeNamespaces {
2✔
73
                inMap[n] = true
1✔
74
        }
1✔
75

76
        // Create and return the new handler
77
        return &SecretsHandler{
1✔
78
                ssClient:           ssCl,
1✔
79
                coreClient:         coreClient,
1✔
80
                disableLoadSecrets: cfg.DisableLoadSecrets,
1✔
81
                includeNamespaces:  inMap,
1✔
82
                config:             cfg,
1✔
83
        }
1✔
84
}
85

86
// NamespacesMatch checks if a namespace is allowed according to the filter rules
87
func (h *SecretsHandler) NamespacesMatch(namespaces []string) map[string]bool {
1✔
88
        matchedNamespaces := make(map[string]bool)
1✔
89
        // If regular expressions should be used for filtering
1✔
90
        if h.config.UseRegex {
2✔
91
                // Process inclusion rules with RegEx
1✔
92
                if len(h.config.IncludeNamespacesRegex) > 0 {
2✔
93
                        for _, r := range h.config.IncludeNamespacesRegex {
2✔
94
                                // Check all namespaces and include those matching the RegEx
1✔
95
                                for _, ns := range namespaces {
2✔
96
                                        matched := r.FindString(ns) == ns
1✔
97
                                        if matched {
2✔
98
                                                matchedNamespaces[ns] = true
1✔
99
                                        }
1✔
100
                                }
101
                        }
102
                } else {
1✔
103
                        // If no inclusion rules, use all namespaces
1✔
104
                        for _, ns := range namespaces {
2✔
105
                                matchedNamespaces[ns] = true
1✔
106
                        }
1✔
107
                }
108

109
                // Process exclusion rules with RegEx
110
                for _, r := range h.config.ExcludeNamespacesRegex {
2✔
111
                        // Remove namespaces that match the exclusion RegEx
1✔
112
                        for ns, _ := range matchedNamespaces {
2✔
113
                                matched := r.FindString(ns) == ns
1✔
114
                                if matched {
2✔
115
                                        // Remove the element from the slice (without preserving order)
1✔
116
                                        matchedNamespaces[ns] = false
1✔
117
                                }
1✔
118
                        }
119
                }
120
        } else {
1✔
121
                // Direct string comparisons for filtering (without RegEx)
1✔
122

1✔
123
                // Add all explicitly included namespaces
1✔
124
                for _, ns := range h.config.IncludeNamespaces {
2✔
125
                        matchedNamespaces[ns] = true
1✔
126
                }
1✔
127

128
                // Apply exclusion logic
129
                if len(h.config.ExcludeNamespaces) > 0 {
2✔
130
                        // If no inclusion rules are defined, use all available namespaces
1✔
131
                        if len(h.config.IncludeNamespaces) < 1 {
2✔
132
                                for _, ns := range namespaces {
2✔
133
                                        matchedNamespaces[ns] = true
1✔
134
                                }
1✔
135
                        }
136

137
                        // Remove namespaces that are in the exclusion list
138
                        for _, exc := range h.config.ExcludeNamespaces {
2✔
139
                                for ns, _ := range matchedNamespaces {
2✔
140
                                        if ns == exc {
2✔
141
                                                matchedNamespaces[ns] = false
1✔
142
                                        }
1✔
143
                                }
144
                        }
145
                }
146
        }
147
        return matchedNamespaces
1✔
148
}
149

150
// list returns a list of all secrets that match the filter criteria
151
func (h *SecretsHandler) list(ctx context.Context) ([]Secret, error) {
1✔
152
        var secrets []Secret
1✔
153

1✔
154
        // If loading secrets is disabled, return an empty list
1✔
155
        if h.disableLoadSecrets {
2✔
156
                return secrets, nil
1✔
157
        }
1✔
158

159
        // If namespace filters are defined (inclusion or exclusion)
160
        if len(h.config.ExcludeNamespaces) > 0 || len(h.config.IncludeNamespaces) > 0 {
2✔
161
                // Exclusion always takes precedence over inclusion
1✔
162
                var nsNameList []string
1✔
163

1✔
164
                // If no inclusion rules are defined, gather all available namespaces
1✔
165
                if len(h.config.IncludeNamespaces) <= 0 || h.config.UseRegex {
2✔
166
                        nsList, err := h.coreClient.Namespaces().List(ctx, metav1.ListOptions{})
1✔
167
                        if err != nil {
1✔
NEW
168
                                return nil, err
×
NEW
169
                        }
×
170
                        for _, namespace := range nsList.Items {
2✔
171
                                nsNameList = append(nsNameList, namespace.Name)
1✔
172
                        }
1✔
173
                } else {
1✔
174
                        nsNameList = h.config.IncludeNamespaces
1✔
175
                }
1✔
176

177
                matchedNamespaces := h.NamespacesMatch(nsNameList)
1✔
178

1✔
179
                // Get secrets for all matching namespaces
1✔
180
                for ns, v := range matchedNamespaces {
2✔
181
                        if !v {
2✔
182
                                continue
1✔
183
                        }
184
                        list, err := h.listForNamespace(ctx, ns)
1✔
185
                        if err != nil {
1✔
186
                                return nil, err
×
187
                        }
×
188
                        secrets = append(secrets, list...)
1✔
189
                }
190
        } else {
1✔
191
                // If no filters are specified, list all secrets in all namespaces
1✔
192
                // Empty string ("") means "all namespaces"
1✔
193
                list, err := h.listForNamespace(ctx, "")
1✔
194
                if err != nil {
1✔
195
                        return nil, err
×
196
                }
×
197
                secrets = append(secrets, list...)
1✔
198
        }
199

200
        // Sort secrets: first by namespace, then by name
201
        sort.Slice(secrets, func(i, j int) bool {
2✔
202
                if secrets[i].Namespace == secrets[j].Namespace {
2✔
203
                        return secrets[i].Name < secrets[j].Name
1✔
204
                }
1✔
205
                return secrets[i].Namespace < secrets[j].Namespace
1✔
206
        })
207

208
        return secrets, nil
1✔
209
}
210

211
// listForNamespace retrieves all Sealed Secrets in a specific namespace
212
func (h *SecretsHandler) listForNamespace(ctx context.Context, ns string) ([]Secret, error) {
1✔
213
        var secrets []Secret
1✔
214

1✔
215
        // API call to retrieve all SealedSecrets in the specified namespace
1✔
216
        // Empty string ("") means "all namespaces"
1✔
217
        ssList, err := h.ssClient.SealedSecrets(ns).List(ctx, metav1.ListOptions{})
1✔
218
        if err != nil {
1✔
219
                return nil, err
×
220
        }
×
221

222
        // Convert SealedSecrets to the simpler Secret structure
223
        for _, item := range ssList.Items {
2✔
224
                secrets = append(secrets, Secret{Namespace: item.Namespace, Name: item.Name})
1✔
225
        }
1✔
226
        return secrets, nil
1✔
227
}
228

229
// GetSecret returns a single secret by namespace and name
230
func (h *SecretsHandler) GetSecret(ctx context.Context, namespace, name string) (*v1.Secret, error) {
×
NEW
231
        // If loading secrets is disabled, return null
×
232
        if h.disableLoadSecrets {
×
233
                return nil, nil
×
234
        }
×
235

236
        // Check if the namespace is allowed according to the filter rules
NEW
237
        if len(h.config.ExcludeNamespaces) > 0 || len(h.config.IncludeNamespaces) > 0 {
×
NEW
238
                namespaces := h.NamespacesMatch([]string{namespace})
×
NEW
239
                if !namespaces[namespace] {
×
NEW
240
                        return nil, fmt.Errorf("namespace '%s' is not allowed", namespace)
×
NEW
241
                }
×
242
        }
243

244
        // Retrieve the secret from the Kubernetes cluster
245
        secret, err := h.coreClient.Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
×
246
        if err != nil {
×
247
                return nil, err
×
248
        }
×
249

250
        // Clean up secret metadata (remove unnecessary fields)
251
        secret.TypeMeta = metav1.TypeMeta{
×
252
                APIVersion: "v1",
×
253
                Kind:       "Secret",
×
254
        }
×
255
        secret.ManagedFields = nil
×
256
        secret.OwnerReferences = nil
×
257
        secret.CreationTimestamp = metav1.Time{}
×
258
        secret.ResourceVersion = ""
×
259
        secret.UID = ""
×
260

×
261
        return secret, nil
×
262
}
263

264
// AllSecrets is an HTTP handler that returns a list of all available secrets
265
func (h *SecretsHandler) AllSecrets(c *gin.Context) {
×
NEW
266
        // If loading secrets is disabled, return an error
×
267
        if h.disableLoadSecrets {
×
268
                c.JSON(http.StatusForbidden, gin.H{"error": "Loading secrets is disabled"})
×
269
                return
×
270
        }
×
271

272
        // Retrieve secrets
273
        sec, err := h.list(c)
×
274
        if err != nil {
×
NEW
275
                // Log error and return it to the client
×
276
                log.Printf("Error in %s: %v\n", Sanitize(c.Request.URL.Path), err)
×
277
                c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
278
                return
×
279
        }
×
280

281
        // Successful response with the list of secrets
UNCOV
282
        c.JSON(http.StatusOK, gin.H{"secrets": sec})
×
283
}
284

285
// Secret is an HTTP handler that returns a single secret
286
func (h *SecretsHandler) Secret(c *gin.Context) {
×
NEW
287
        // Determine the response format (JSON or YAML)
×
288
        contentType, outputFormat, done := NegotiateFormat(c)
×
289
        if done {
×
NEW
290
                return // If the format is not supported, an error response was already sent
×
291
        }
×
292

293
        // If loading secrets is disabled, return an error
294
        if h.disableLoadSecrets {
×
295
                c.JSON(http.StatusForbidden, gin.H{"error": "Loading secrets is disabled"})
×
296
                return
×
297
        }
×
298

299
        // Extract and sanitize parameters from the request
300
        namespace := Sanitize(c.Param("namespace"))
×
301
        name := Sanitize(c.Param("name"))
×
NEW
302

×
NEW
303
        // Retrieve the secret
×
304
        secret, err := h.GetSecret(c, namespace, name)
×
305
        if err != nil {
×
NEW
306
                // Log error and return it to the client
×
307
                log.Printf("Error in %s: %v\n", Sanitize(c.Request.URL.Path), err)
×
308
                c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
309
                return
×
310
        }
×
311

312
        // Encode the secret in the desired format
313
        encode, err := encodeSecret(secret, outputFormat)
×
314
        if err != nil {
×
NEW
315
                // Log encoding error and return it
×
316
                log.Printf("Error in %s: %v\n", Sanitize(c.Request.URL.Path), err)
×
317
                c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
318
                return
×
319
        }
×
320

321
        // Successful response with the encoded secret
UNCOV
322
        c.Data(http.StatusOK, contentType, encode)
×
323
}
324

325
// encodeSecret encodes a Secret object into the specified format (JSON or YAML)
326
func encodeSecret(secret *v1.Secret, outputFormat string) ([]byte, error) {
1✔
327
        var contentType string
1✔
328

1✔
329
        // Determine content type based on the desired output format
1✔
330
        switch strings.ToLower(outputFormat) {
1✔
331
        case "json", "":
1✔
332
                contentType = runtime.ContentTypeJSON
1✔
333
        case "yaml":
1✔
334
                contentType = runtime.ContentTypeYAML
1✔
335
        default:
×
336
                return nil, fmt.Errorf("unsupported output format: %s", outputFormat)
×
337
        }
338

339
        // Get serializer for the desired format
340
        info, ok := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), contentType)
1✔
341
        if !ok {
1✔
342
                return nil, fmt.Errorf("unsupported output format: %s", outputFormat)
×
343
        }
×
344

345
        // Use "pretty" serializer if available, otherwise use standard serializer
346
        prettyEncoder := info.PrettySerializer
1✔
347
        if prettyEncoder == nil {
2✔
348
                prettyEncoder = info.Serializer
1✔
349
        }
1✔
350

351
        // Create encoder for the API version
352
        encoder := scheme.Codecs.EncoderForVersion(prettyEncoder, schema.GroupVersion{Group: "", Version: "v1"})
1✔
353

1✔
354
        // Encode and return the secret
1✔
355
        return runtime.Encode(encoder, secret)
1✔
356
}
357

358
// Secret represents the basic data of a Kubernetes Secret
359
type Secret struct {
360
        Namespace string `json:"namespace" yaml:"namespace"` // Namespace where the secret is located
361
        Name      string `json:"name"      yaml:"name"`      // Name of the secret
362
}
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