Skip to content

Commit 16da91d

Browse files
authored
feature(inject): introduce PatchProducer abstraction (#14175)
This PR introduces the `PatchProducer` abstraction. This allows to abstract away the creation of proxy injection patches and to potentially layer multiple patches on top of each other. Signed-off-by: Zahari Dichev <[email protected]>
1 parent f41fcac commit 16da91d

File tree

9 files changed

+417
-46
lines changed

9 files changed

+417
-46
lines changed

cli/cmd/inject.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
207207
conf.AppendPodAnnotation(k8s.ProxyInjectAnnotation, k8s.ProxyInjectEnabled)
208208
}
209209

210-
patchJSON, err := conf.GetPodPatch(rt.injectProxy, rt.overrider)
210+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, rt.injectProxy, rt.overrider)
211211
if err != nil {
212212
return nil, nil, err
213213
}

controller/cmd/proxy-injector/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func Main(args []string) {
2525
webhook.Launch(
2626
context.Background(),
2727
[]k8s.APIResource{k8s.NS, k8s.Deploy, k8s.RC, k8s.RS, k8s.Job, k8s.DS, k8s.SS, k8s.Pod, k8s.CJ},
28-
injector.Inject(*linkerdNamespace, inject.GetOverriddenValues),
28+
injector.Inject(*linkerdNamespace, inject.GetOverriddenValues, inject.PatchProducers),
2929
"linkerd-proxy-injector",
3030
*metricsAddr,
3131
*addr,

controller/proxy-injector/webhook.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const (
2828
// Inject returns the function that produces an AdmissionResponse containing
2929
// the patch, if any, to apply to the pod (proxy sidecar and eventually the
3030
// init container to set it up)
31-
func Inject(linkerdNamespace string, overrider inject.ValueOverrider) webhook.Handler {
31+
func Inject(linkerdNamespace string, overrider inject.ValueOverrider, patchProducers []inject.PatchProducer) webhook.Handler {
3232
return func(
3333
ctx context.Context,
3434
api *k8s.MetadataAPI,
@@ -116,10 +116,11 @@ func Inject(linkerdNamespace string, overrider inject.ValueOverrider) webhook.Ha
116116
}
117117
}
118118

119-
patchJSON, err := resourceConfig.GetPodPatch(true, overrider)
119+
patchJSON, err := inject.ProduceMergedPatch(patchProducers, resourceConfig, true, overrider)
120120
if err != nil {
121121
return nil, err
122122
}
123+
123124
if parent != nil {
124125
recorder.Event(parent, v1.EventTypeNormal, eventTypeInjected, "Linkerd sidecar proxy injected")
125126
}

controller/proxy-injector/webhook_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func TestGetPodPatch(t *testing.T) {
119119
t.Fatal(err)
120120
}
121121

122-
patchJSON, err := fullConf.GetPodPatch(true, inject.GetOverriddenValues)
122+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, fullConf, true, inject.GetOverriddenValues)
123123
if err != nil {
124124
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
125125
}
@@ -142,7 +142,7 @@ func TestGetPodPatch(t *testing.T) {
142142
t.Fatal(err)
143143
}
144144

145-
patchJSON, err := conf.GetPodPatch(true, inject.GetOverriddenValues)
145+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, true, inject.GetOverriddenValues)
146146
if err != nil {
147147
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
148148
}
@@ -163,7 +163,7 @@ func TestGetPodPatch(t *testing.T) {
163163
t.Fatal(err)
164164
}
165165

166-
patchJSON, err := conf.GetPodPatch(true, inject.GetOverriddenValues)
166+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, true, inject.GetOverriddenValues)
167167
if err != nil {
168168
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
169169
}
@@ -186,7 +186,7 @@ func TestGetPodPatch(t *testing.T) {
186186
t.Fatal(err)
187187
}
188188

189-
patchJSON, err := conf.GetPodPatch(true, inject.GetOverriddenValues)
189+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, true, inject.GetOverriddenValues)
190190
if err != nil {
191191
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
192192
}
@@ -210,7 +210,7 @@ func TestGetPodPatch(t *testing.T) {
210210
t.Fatal(err)
211211
}
212212

213-
patchJSON, err := conf.GetPodPatch(true, inject.GetOverriddenValues)
213+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, true, inject.GetOverriddenValues)
214214
if err != nil {
215215
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
216216
}
@@ -240,7 +240,7 @@ func TestGetPodPatch(t *testing.T) {
240240
// The namespace has two config annotations: one valid and one invalid
241241
// the pod patch should only contain the valid annotation.
242242
inject.AppendNamespaceAnnotations(conf.GetOverrideAnnotations(), conf.GetNsAnnotations(), conf.GetWorkloadAnnotations())
243-
patchJSON, err := conf.GetPodPatch(true, inject.GetOverriddenValues)
243+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, true, inject.GetOverriddenValues)
244244
if err != nil {
245245
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
246246
}
@@ -254,7 +254,7 @@ func TestGetPodPatch(t *testing.T) {
254254
deployment := fileContents(factory, t, "deployment-with-injected-proxy.yaml")
255255
fakeReq := getFakePodReq(deployment)
256256
conf := confNsDisabled().WithKind(fakeReq.Kind.Kind)
257-
patchJSON, err := conf.GetPodPatch(true, inject.GetOverriddenValues)
257+
patchJSON, err := inject.ProduceMergedPatch(inject.PatchProducers, conf, true, inject.GetOverriddenValues)
258258
if err != nil {
259259
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
260260
}

pkg/inject/inject.go

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"html/template"
11-
"net"
1211
"reflect"
1312
"regexp"
1413
"sort"
@@ -90,7 +89,19 @@ var (
9089
}
9190
)
9291

93-
type ValueOverrider func(values *l5dcharts.Values, overrides map[string]string, namedPorts map[string]int32) (*l5dcharts.Values, error)
92+
// OverriddenValues contains the result of executing an instance of ValueOverrider
93+
type OverriddenValues struct {
94+
*l5dcharts.Values
95+
96+
// may contain additional values that are not part of the l5dcharts.Values
97+
// in order to allow custom ValueOverrider implementations to add their own
98+
// values to the rendering logic.
99+
Additional map[string]interface{}
100+
}
101+
102+
// ValueOverrider is used to override the default values that are used in chart rendering based
103+
// on the annotations provided in overrides.
104+
type ValueOverrider func(values *l5dcharts.Values, overrides map[string]string, namedPorts map[string]int32) (*OverriddenValues, error)
94105

95106
// Origin defines where the input YAML comes from. Refer the ResourceConfig's
96107
// 'origin' field
@@ -188,15 +199,15 @@ func AppendNamespaceAnnotations(base map[string]string, nsAnn map[string]string,
188199

189200
// GetOverriddenValues returns the final Values struct which is created
190201
// by overriding annotated configuration on top of default Values
191-
func GetOverriddenValues(values *l5dcharts.Values, overrides map[string]string, namedPorts map[string]int32) (*l5dcharts.Values, error) {
202+
func GetOverriddenValues(values *l5dcharts.Values, overrides map[string]string, namedPorts map[string]int32) (*OverriddenValues, error) {
192203
// Make a copy of Values and mutate that
193204
copyValues, err := values.DeepCopy()
194205
if err != nil {
195206
return nil, err
196207
}
197208

198209
applyAnnotationOverrides(copyValues, overrides, namedPorts)
199-
return copyValues, nil
210+
return &OverriddenValues{Values: copyValues}, nil
200211
}
201212

202213
func applyAnnotationOverrides(values *l5dcharts.Values, annotations map[string]string, namedPorts map[string]int32) {
@@ -663,37 +674,12 @@ func (conf *ResourceConfig) getAnnotationOverrides() map[string]string {
663674

664675
// GetPodPatch returns the JSON patch containing the proxy and init containers specs, if any.
665676
// If injectProxy is false, only the config.linkerd.io annotations are set.
666-
func (conf *ResourceConfig) GetPodPatch(injectProxy bool, overrider ValueOverrider) ([]byte, error) {
667-
namedPorts := make(map[string]int32)
668-
if conf.HasPodTemplate() {
669-
namedPorts = util.GetNamedPorts(conf.pod.spec.Containers)
670-
}
671-
672-
values, err := overrider(conf.values, conf.getAnnotationOverrides(), namedPorts)
673-
values.Proxy.PodInboundPorts = getPodInboundPorts(conf.pod.spec)
674-
if err != nil {
675-
return nil, fmt.Errorf("could not generate Overridden Values: %w", err)
676-
}
677-
678-
if values.ClusterNetworks != "" {
679-
for _, network := range strings.Split(strings.Trim(values.ClusterNetworks, ","), ",") {
680-
if _, _, err := net.ParseCIDR(network); err != nil {
681-
return nil, fmt.Errorf("cannot parse destination get networks: %w", err)
682-
}
683-
}
684-
}
685-
677+
func GetPodPatch(conf *ResourceConfig, injectProxy bool, values *OverriddenValues, patchPathPrefix string) ([]JSONPatch, error) {
686678
patch := &podPatch{
687-
Values: *values,
679+
Values: *values.Values,
688680
Annotations: map[string]string{},
689681
Labels: map[string]string{},
690-
}
691-
switch strings.ToLower(conf.workload.metaType.Kind) {
692-
case k8s.Pod:
693-
case k8s.CronJob:
694-
patch.PathPrefix = "/spec/jobTemplate/spec/template"
695-
default:
696-
patch.PathPrefix = "/spec/template"
682+
PathPrefix: patchPathPrefix,
697683
}
698684

699685
if conf.pod.spec != nil {
@@ -734,7 +720,12 @@ func (conf *ResourceConfig) GetPodPatch(injectProxy bool, overrider ValueOverrid
734720
// Get rid of invalid trailing commas
735721
res := rTrail.ReplaceAll(buf.Bytes(), []byte("}\n]"))
736722

737-
return res, nil
723+
patchResult := []JSONPatch{}
724+
if err := json.Unmarshal(res, &patchResult); err != nil {
725+
return nil, err
726+
}
727+
728+
return patchResult, nil
738729
}
739730

740731
// GetConfigAnnotation returns two values. The first value is the annotation

pkg/inject/inject_fuzzer.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package inject
22

33
import (
44
l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
5+
"github.com/linkerd/linkerd2/pkg/util"
56

67
fuzz "github.com/AdaLogics/go-fuzz-headers"
78
)
@@ -25,7 +26,18 @@ func FuzzInject(data []byte) int {
2526
if err != nil {
2627
return 0
2728
}
28-
_, _ = conf.GetPodPatch(injectProxy, GetOverriddenValues)
29+
30+
namedPorts := make(map[string]int32)
31+
if conf.HasPodTemplate() {
32+
namedPorts = util.GetNamedPorts(conf.pod.spec.Containers)
33+
}
34+
35+
values, err := GetOverriddenValues(conf.values, conf.getAnnotationOverrides(), namedPorts)
36+
if err != nil {
37+
return 0
38+
}
39+
40+
_, _ = GetPodPatch(conf, injectProxy, values, getPatchPathPrefix(conf))
2941
_, _ = conf.CreateOpaquePortsPatch()
3042

3143
report := &Report{}

pkg/inject/inject_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func TestGetOverriddenValues(t *testing.T) {
335335
t.Fatal(err)
336336
}
337337
expected := testCase.expected()
338-
if diff := deep.Equal(actual, expected); diff != nil {
338+
if diff := deep.Equal(actual.Values, expected); diff != nil {
339339
t.Errorf("%+v", diff)
340340
}
341341
})

pkg/inject/patch.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package inject
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net"
7+
"strings"
8+
9+
"github.com/linkerd/linkerd2/pkg/k8s"
10+
"github.com/linkerd/linkerd2/pkg/util"
11+
)
12+
13+
// PatchProducers is the default set of patch producers used to generate Linkerd injection patches.
14+
var PatchProducers = []PatchProducer{GetPodPatch}
15+
16+
// PatchProducer is a function that generates a patch for a given resource configuration
17+
// and OverriddenValues.
18+
type PatchProducer func(conf *ResourceConfig, injectProxy bool, values *OverriddenValues, patchPathPrefix string) ([]JSONPatch, error)
19+
20+
// JSONPatch format is specified in RFC 6902
21+
type JSONPatch struct {
22+
Operation string `json:"op"`
23+
Path string `json:"path"`
24+
Value interface{} `json:"value,omitempty"`
25+
}
26+
27+
func getPatchPathPrefix(conf *ResourceConfig) string {
28+
switch strings.ToLower(conf.workload.metaType.Kind) {
29+
case k8s.Pod:
30+
return ""
31+
case k8s.CronJob:
32+
return "/spec/jobTemplate/spec/template"
33+
default:
34+
return "/spec/template"
35+
}
36+
}
37+
38+
// ProduceMergedPatch executes the provided PatchProducers to generate a merged JSON patch that combines
39+
// all generated patches.
40+
func ProduceMergedPatch(producers []PatchProducer, conf *ResourceConfig, injectProxy bool, overrider ValueOverrider) ([]byte, error) {
41+
namedPorts := make(map[string]int32)
42+
if conf.HasPodTemplate() {
43+
namedPorts = util.GetNamedPorts(conf.pod.spec.Containers)
44+
}
45+
46+
values, err := overrider(conf.values, conf.getAnnotationOverrides(), namedPorts)
47+
if err != nil {
48+
return nil, fmt.Errorf("could not generate Overridden Values: %w", err)
49+
}
50+
51+
values.Proxy.PodInboundPorts = getPodInboundPorts(conf.pod.spec)
52+
if err != nil {
53+
return nil, fmt.Errorf("could not generate Overridden Values: %w", err)
54+
}
55+
56+
if values.ClusterNetworks != "" {
57+
for _, network := range strings.Split(strings.Trim(values.ClusterNetworks, ","), ",") {
58+
if _, _, err := net.ParseCIDR(network); err != nil {
59+
return nil, fmt.Errorf("cannot parse destination get networks: %w", err)
60+
}
61+
}
62+
}
63+
64+
merged := []JSONPatch{}
65+
for _, producer := range producers {
66+
patch, err := producer(conf, injectProxy, values, getPatchPathPrefix(conf))
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
// If the patch is empty, skip it
72+
if len(patch) == 0 {
73+
continue
74+
}
75+
76+
merged = append(merged, patch...)
77+
}
78+
79+
return json.Marshal(merged)
80+
}

0 commit comments

Comments
 (0)