Skip to content

Commit 77dc6df

Browse files
authored
Merge pull request #204 from jetstack/fieldfilter
Implement more generic means of filtering uploaded objects
2 parents caf52d5 + 0521da0 commit 77dc6df

File tree

6 files changed

+283
-53
lines changed

6 files changed

+283
-53
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ require (
88
github.com/Azure/go-autorest/autorest v0.11.8 // indirect
99
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
1010
github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect
11+
github.com/Jeffail/gabs v1.1.1
12+
github.com/Jeffail/gabs/v2 v2.6.0
1113
github.com/aws/aws-sdk-go v1.34.10
1214
github.com/cenkalti/backoff v2.0.0+incompatible
1315
github.com/d4l3k/messagediff v1.2.1

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
9090
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
9191
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
9292
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
93+
github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo=
94+
github.com/Jeffail/gabs/v2 v2.6.0 h1:WdCnGaDhNa4LSRTMwhLZzJ7SRDXjABNP13SOKvCpL5w=
95+
github.com/Jeffail/gabs/v2 v2.6.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
9396
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
9497
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
9598
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=

pkg/datagatherer/k8s/dynamic.go

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -164,69 +164,33 @@ func (g *DataGathererDynamic) Fetch() (interface{}, error) {
164164
}
165165

166166
func redactList(list *unstructured.UnstructuredList) error {
167-
// In principal we could only redact the list if it's kind is SecretList or
168-
// a generic mixed List, however the test suite does not set the list kind
169-
// and it is safer to always check for Secrets.
170167
for i := range list.Items {
171168
// Determine the kind of items in case this is a generic 'mixed' list.
172169
gvks, _, err := scheme.Scheme.ObjectKinds(&list.Items[i])
173170
if err != nil {
174171
return errors.WithStack(err)
175172
}
176173

177-
object := list.Items[i]
174+
resource := list.Items[i]
178175

179176
for _, gvk := range gvks {
180177
// If this item is a Secret then we need to redact it.
181178
if gvk.Kind == "Secret" && (gvk.Group == "core" || gvk.Group == "") {
179+
Select(SecretSelectedFields, &resource)
182180

183-
// If the secret is a tls secret, we redact all data other then
184-
// the tls.crt and ca.crt. This is because we need to inspect
185-
// the certificate to make recommendations.
186-
if object.Object["type"] == "kubernetes.io/tls" {
187-
secretData, ok := object.Object["data"].(map[string]interface{})
188-
if ok {
189-
for k := range secretData {
190-
// Only these two keys will be sent, all others are
191-
// deleted
192-
if k != "tls.crt" && k != "ca.crt" {
193-
delete(secretData, k)
194-
}
195-
}
196-
} else {
197-
// If secret is not string mapping, redact all secret data
198-
object.Object["data"] = map[string]interface{}{}
199-
}
200-
} else {
201-
// Redact all secret data for non-tls secrets
202-
object.Object["data"] = map[string]interface{}{}
203-
}
204-
205-
metadata, metadataPresent := object.Object["metadata"].(map[string]interface{})
206-
if metadataPresent {
207-
// Redact last-applied-configuration annotation if set
208-
annotations, present := metadata["annotations"].(map[string]interface{})
209-
if present {
210-
_, annotationPresent := annotations["kubectl.kubernetes.io/last-applied-configuration"]
211-
if annotationPresent {
212-
annotations["kubectl.kubernetes.io/last-applied-configuration"] = "redacted"
213-
}
214-
metadata["annotations"] = annotations
215-
}
216-
}
217181
// break when the object has been processed as a secret, no
218182
// other kinds have redact modifications
219183
break
220184
}
221185

222-
metadata, metadataPresent := object.Object["metadata"].(map[string]interface{})
223-
if metadataPresent {
224-
// Drop managed fields if set
225-
if _, present := metadata["managedFields"]; present {
226-
delete(metadata, "managedFields")
227-
}
228-
}
186+
// remove managedFields from all resources
187+
Redact([]string{
188+
"metadata.managedFields",
189+
}, &resource)
229190
}
191+
192+
// update the object in the list
193+
list.Items[i] = resource
230194
}
231195
return nil
232196
}

pkg/datagatherer/k8s/dynamic_test.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ func getObject(version, kind, name, namespace string, withManagedFields bool) *u
4141

4242
func getSecret(name, namespace string, data map[string]interface{}, isTLS bool, withLastApplied bool) *unstructured.Unstructured {
4343
object := getObject("v1", "Secret", name, namespace, false)
44-
object.Object["data"] = data
44+
45+
if data != nil {
46+
object.Object["data"] = data
47+
}
4548

4649
object.Object["type"] = "Opaque"
4750
if isTLS {
@@ -56,10 +59,6 @@ func getSecret(name, namespace string, data map[string]interface{}, isTLS bool,
5659
metadata["annotations"] = map[string]interface{}{
5760
"kubectl.kubernetes.io/last-applied-configuration": string(jsonData),
5861
}
59-
} else { // generate an expected redacted secret
60-
metadata["annotations"] = map[string]interface{}{
61-
"kubectl.kubernetes.io/last-applied-configuration": "redacted",
62-
}
6362
}
6463

6564
return object
@@ -164,8 +163,8 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
164163
}, false, true),
165164
},
166165
expected: asUnstructuredList(
167-
getSecret("testsecret", "testns1", map[string]interface{}{}, false, false),
168-
getSecret("anothertestsecret", "testns2", map[string]interface{}{}, false, false),
166+
getSecret("testsecret", "testns1", nil, false, false),
167+
getSecret("anothertestsecret", "testns2", nil, false, false),
169168
),
170169
},
171170
"Secret of type kubernetes.io/tls should have crts and not keys": {
@@ -188,7 +187,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
188187
"ca.crt": "value",
189188
}, true, false),
190189
// all other keys removed
191-
getSecret("anothertestsecret", "testns2", map[string]interface{}{}, true, false),
190+
getSecret("anothertestsecret", "testns2", nil, true, false),
192191
),
193192
},
194193
"Foos in different namespaces should be returned if they are in the namespace list for the gatherer": {
@@ -240,6 +239,9 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
240239
}
241240
if diff, equal := messagediff.PrettyDiff(test.expected, res); !equal {
242241
t.Errorf("\n%s", diff)
242+
expectedJSON, _ := json.MarshalIndent(test.expected, "", " ")
243+
gotJSON, _ := json.MarshalIndent(res, "", " ")
244+
t.Fatalf("unexpected JSON: \ngot \n%s\nwant\n%s", string(gotJSON), expectedJSON)
243245
}
244246
})
245247
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package k8s
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/Jeffail/gabs/v2"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
)
11+
12+
// SecretSelectedFields is the list of fields sent from Secret objects to the
13+
// backend
14+
var SecretSelectedFields = []string{
15+
"kind",
16+
"apiVersion",
17+
"metadata.name",
18+
"metadata.namespace",
19+
"type",
20+
"/data/tls.crt",
21+
"/data/ca.crt",
22+
}
23+
24+
// Select removes all but the supplied fields from the resource
25+
func Select(fields []string, resource *unstructured.Unstructured) error {
26+
// convert the object to JSON for field filtering
27+
asJSON, err := json.Marshal(resource)
28+
if err != nil {
29+
return fmt.Errorf("failed to marshal json for resource: %s", err)
30+
}
31+
32+
// parse the JSON for processing in gabs
33+
jsonParsed, err := gabs.ParseJSON(asJSON)
34+
if err != nil {
35+
return fmt.Errorf("failed to parse generated json for resource: %s", err)
36+
}
37+
38+
// craft a new object containing only selected fields
39+
filteredObject := gabs.New()
40+
for _, v := range fields {
41+
// also support JSONPointers for keys containing '.' chars
42+
if strings.HasPrefix(v, "/") {
43+
gObject, err := jsonParsed.JSONPointer(v)
44+
if err != nil {
45+
// fail to select field if missing, just continue
46+
continue
47+
}
48+
pathComponents := strings.Split(v, "/")
49+
filteredObject.Set(gObject.Data(), pathComponents[1:]...)
50+
} else {
51+
if jsonParsed.ExistsP(v) {
52+
filteredObject.SetP(jsonParsed.Path(v).Data(), v)
53+
}
54+
}
55+
}
56+
57+
// load the filtered JSON back into the resource
58+
err = json.Unmarshal(filteredObject.Bytes(), resource)
59+
if err != nil {
60+
return fmt.Errorf("failed to update resource: %s", err)
61+
}
62+
63+
return nil
64+
}
65+
66+
// Redact removes the supplied fields from the resource
67+
func Redact(fields []string, resource *unstructured.Unstructured) error {
68+
// convert the object to JSON for field filtering
69+
asJSON, err := json.Marshal(resource)
70+
if err != nil {
71+
return fmt.Errorf("failed to marshal json for resource: %s", err)
72+
}
73+
74+
// parse the JSON for processing in gabs
75+
jsonParsed, err := gabs.ParseJSON(asJSON)
76+
if err != nil {
77+
return fmt.Errorf("failed to parse generated json for resource: %s", err)
78+
}
79+
80+
// craft a new object excluding redacted fields
81+
for _, v := range fields {
82+
// also support JSONPointers for keys containing '.' chars
83+
if strings.HasPrefix(v, "/") {
84+
pathComponents := strings.Split(v, "/")[1:]
85+
if jsonParsed.Exists(pathComponents...) {
86+
jsonParsed.Delete(pathComponents...)
87+
}
88+
} else {
89+
if jsonParsed.ExistsP(v) {
90+
jsonParsed.DeleteP(v)
91+
}
92+
}
93+
}
94+
95+
// load the filtered JSON back into the resource
96+
err = json.Unmarshal(jsonParsed.Bytes(), resource)
97+
if err != nil {
98+
return fmt.Errorf("failed to update resource: %s", err)
99+
}
100+
101+
return nil
102+
}

0 commit comments

Comments
 (0)