Skip to content
This repository was archived by the owner on Jul 10, 2024. It is now read-only.

Commit 771d368

Browse files
authored
Add utilities for Injecting Kubernetes Deployments (#207)
This PR adds utilities for injecting Kubernetes deployments to be used with #206. The main component is the `Injector` interface which provides the functionality to traverse YAML files (including those with multiple resources using the `---` directive), and inject sidecar containers into any found Deployments.
1 parent a87238f commit 771d368

File tree

6 files changed

+725
-31
lines changed

6 files changed

+725
-31
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package injector
2+
3+
import (
4+
"bytes"
5+
v1 "k8s.io/api/core/v1"
6+
kyaml "sigs.k8s.io/yaml"
7+
)
8+
9+
// Calls the given injector's Inject method and returns the result as a YAML bytes.
10+
func ToRawYAML(injector Injector, sidecar v1.Container) ([]byte, error) {
11+
injectedObjects, err := injector.Inject(sidecar)
12+
if err != nil {
13+
return nil, err
14+
}
15+
16+
out := new(bytes.Buffer)
17+
for _, obj := range injectedObjects {
18+
raw, err := kyaml.Marshal(obj)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
out.WriteString("---\n")
24+
out.Write(raw)
25+
}
26+
27+
return out.Bytes(), nil
28+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package injector
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/akitasoftware/go-utils/sets"
11+
"github.com/akitasoftware/go-utils/slices"
12+
"github.com/pkg/errors"
13+
appsv1 "k8s.io/api/apps/v1"
14+
v1 "k8s.io/api/core/v1"
15+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
16+
"k8s.io/apimachinery/pkg/runtime"
17+
"k8s.io/apimachinery/pkg/runtime/schema"
18+
kyamlutil "k8s.io/apimachinery/pkg/util/yaml"
19+
)
20+
21+
type (
22+
Injector interface {
23+
// Injects the given sidecar into all valid Deployment Objects and returns the result as a list of unstructured objects.
24+
Inject(sidecar v1.Container) ([]*unstructured.Unstructured, error)
25+
// Returns a list of namespaces that contain injectable objects.
26+
// This can be used to generate other Kuberenetes objects that need to be created in the same namespace.
27+
InjectableNamespaces() ([]string, error)
28+
}
29+
injectorImpl struct {
30+
// The list of Kubernetes objects to traverse during injection. This is a list of
31+
// unstructured objects because we likely won't know the type of all objects
32+
// ahead of time (e.g., when reading multiple objects from a YAML file).
33+
objects []*unstructured.Unstructured
34+
}
35+
)
36+
37+
// Constructs a new Injector with Kubernetes objects derived from the given file path.
38+
func FromYAML(filePath string) (Injector, error) {
39+
var err error
40+
yamlContent, err := getFile(filePath)
41+
if err != nil {
42+
return nil, errors.Wrap(err, "failed to retrieve raw yaml file content")
43+
}
44+
45+
// Read the YAML file into a list of unstructured objects.
46+
// This is necessary because the YAML file may contain multiple Kubernetes objects.
47+
// We only want to inject the sidecar into Deployment objects, but we still need to parse all resources.
48+
multidocReader := kyamlutil.NewYAMLReader(bufio.NewReader(bytes.NewReader(yamlContent)))
49+
50+
var objList []*unstructured.Unstructured
51+
for {
52+
raw, err := multidocReader.Read()
53+
if err != nil {
54+
if errors.Is(err, io.EOF) {
55+
break
56+
}
57+
return nil, errors.Wrap(err, "failed to read raw yaml file")
58+
}
59+
60+
obj, err := fromRawObject(raw)
61+
if err != nil {
62+
return nil, errors.Wrap(err, "failed to convert raw yaml resource to an unstructured object")
63+
}
64+
65+
objList = append(objList, obj)
66+
}
67+
68+
return &injectorImpl{objects: objList}, nil
69+
}
70+
71+
func (i *injectorImpl) InjectableNamespaces() ([]string, error) {
72+
set := sets.NewSet[string]()
73+
74+
for _, obj := range i.objects {
75+
gvk := obj.GetObjectKind().GroupVersionKind()
76+
77+
if !isInjectable(gvk) {
78+
continue
79+
}
80+
81+
deployment, err := toDeployment(obj)
82+
if err != nil {
83+
return nil, errors.Wrap(err, "failed to convert object to deployment during namespace discovery")
84+
}
85+
86+
if deployment.Namespace == "" {
87+
set.Insert("default")
88+
} else {
89+
set.Insert(deployment.Namespace)
90+
}
91+
}
92+
93+
return set.AsSlice(), nil
94+
}
95+
96+
func (i *injectorImpl) Inject(sidecar v1.Container) ([]*unstructured.Unstructured, error) {
97+
onMap := func(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
98+
gvk := obj.GetObjectKind().GroupVersionKind()
99+
100+
if !isInjectable(gvk) {
101+
return obj, nil
102+
}
103+
104+
deployment, err := toDeployment(obj)
105+
if err != nil {
106+
return nil, errors.Wrap(err, "failed to convert object to deployment during injection")
107+
}
108+
109+
containers := deployment.Spec.Template.Spec.Containers
110+
deployment.Spec.Template.Spec.Containers = append(containers, sidecar)
111+
112+
obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(deployment)
113+
if err != nil {
114+
return nil, errors.Wrap(err, "failed to convert injected deployment to unstructured object")
115+
}
116+
117+
return obj, nil
118+
}
119+
120+
return slices.MapWithErr(i.objects, onMap)
121+
}
122+
123+
func isInjectable(kind schema.GroupVersionKind) bool {
124+
acceptedKind := schema.GroupVersionKind{
125+
Group: "apps",
126+
Version: "v1",
127+
Kind: "Deployment",
128+
}
129+
130+
return kind == acceptedKind
131+
}
132+
133+
// Converts a generic Kubernetes object into a Deployment Object.
134+
func toDeployment(obj *unstructured.Unstructured) (*appsv1.Deployment, error) {
135+
var deployment *appsv1.Deployment
136+
137+
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment)
138+
if err != nil {
139+
return nil, errors.Wrap(err, "failed to convert object to deployment during injection")
140+
}
141+
142+
return deployment, nil
143+
}
144+
145+
// fromRawObject converts raw bytes into an unstructured.Unstrucutred object.
146+
// unstructured.Unstructured is used to represent a Kubernetes object that is not known ahead of time.
147+
func fromRawObject(raw []byte) (*unstructured.Unstructured, error) {
148+
jConfigMap, err := kyamlutil.ToJSON(raw)
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
object, err := runtime.Decode(unstructured.UnstructuredJSONScheme, jConfigMap)
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
unstruct, ok := object.(*unstructured.Unstructured)
159+
if !ok {
160+
return nil, errors.New("unstructured conversion failed")
161+
}
162+
163+
return unstruct, nil
164+
}
165+
166+
func getFile(filePath string) ([]byte, error) {
167+
fileDir, fileName := filepath.Split(filePath)
168+
169+
absOutputDir, err := filepath.Abs(fileDir)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
// Check for directory existence
175+
if _, staterr := os.Stat(absOutputDir); os.IsNotExist(staterr) {
176+
return nil, errors.Wrapf(staterr, "directory %s does not exist", absOutputDir)
177+
}
178+
179+
absPath := filepath.Join(absOutputDir, fileName)
180+
181+
// Check for existence of file
182+
if _, staterr := os.Stat(absPath); os.IsNotExist(staterr) {
183+
return nil, errors.Wrapf(staterr, "file %s does not exist", absPath)
184+
}
185+
186+
return os.ReadFile(absPath)
187+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package injector
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
appsv1 "k8s.io/api/apps/v1"
6+
v1 "k8s.io/api/core/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"testing"
11+
)
12+
13+
func Test_Inject(t *testing.T) {
14+
toUnstructured := func(obj runtime.Object) *unstructured.Unstructured {
15+
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
16+
if err != nil {
17+
panic(err)
18+
}
19+
20+
return &unstructured.Unstructured{Object: u}
21+
}
22+
23+
appendContainer := func(deployment *appsv1.Deployment, container v1.Container) *appsv1.Deployment {
24+
injectedDeployment := deployment.DeepCopy()
25+
containers := injectedDeployment.Spec.Template.Spec.Containers
26+
injectedDeployment.Spec.Template.Spec.Containers = append(containers, container)
27+
28+
return injectedDeployment
29+
}
30+
31+
// GIVEN
32+
dummyDeployment1 := &appsv1.Deployment{
33+
TypeMeta: metav1.TypeMeta{
34+
Kind: "Deployment",
35+
APIVersion: "apps/v1",
36+
},
37+
ObjectMeta: metav1.ObjectMeta{
38+
Name: "test-deploy-1",
39+
},
40+
Spec: appsv1.DeploymentSpec{
41+
Template: v1.PodTemplateSpec{
42+
Spec: v1.PodSpec{
43+
Containers: []v1.Container{
44+
{
45+
Name: "nginx",
46+
Image: "nginx",
47+
},
48+
},
49+
},
50+
},
51+
},
52+
}
53+
dummyDeployment2 := &appsv1.Deployment{
54+
TypeMeta: metav1.TypeMeta{
55+
Kind: "Deployment",
56+
APIVersion: "apps/v1",
57+
},
58+
ObjectMeta: metav1.ObjectMeta{
59+
Name: "test-deploy-2",
60+
},
61+
Spec: appsv1.DeploymentSpec{
62+
Template: v1.PodTemplateSpec{
63+
Spec: v1.PodSpec{
64+
Containers: []v1.Container{
65+
{
66+
Name: "echo-server",
67+
Image: "ghcr.io/wzshiming/echoserver/echoserver:v0.0.1",
68+
},
69+
},
70+
},
71+
},
72+
},
73+
}
74+
75+
sidecar := v1.Container{Name: "sidecar", Image: "fake-image"}
76+
expectedDeployment1 := appendContainer(dummyDeployment1, sidecar)
77+
expectedDeployment2 := appendContainer(dummyDeployment2, sidecar)
78+
79+
injector := injectorImpl{
80+
objects: []*unstructured.Unstructured{
81+
toUnstructured(dummyDeployment1),
82+
toUnstructured(dummyDeployment2),
83+
},
84+
}
85+
86+
expected := []*unstructured.Unstructured{
87+
toUnstructured(expectedDeployment1),
88+
toUnstructured(expectedDeployment2),
89+
}
90+
91+
// WHEN
92+
actual, err := injector.Inject(sidecar)
93+
94+
// THEN
95+
if assert.NoError(t, err) {
96+
assert.Equal(t, expected, actual)
97+
}
98+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: test-deploy
5+
namespace: default
6+
spec:
7+
replicas: 1
8+
selector:
9+
matchLabels:
10+
app: test-pod
11+
template:
12+
metadata:
13+
labels:
14+
app: test-pod
15+
spec:
16+
containers:
17+
- name: test-container
18+
image: ghcr.io/wzshiming/echoserver/echoserver:v0.0.1
19+
---
20+
apiVersion: apps/v1
21+
kind: Deployment
22+
metadata:
23+
name: patch-demo
24+
namespace: ns1
25+
spec:
26+
replicas: 2
27+
selector:
28+
matchLabels:
29+
app: nginx
30+
template:
31+
metadata:
32+
labels:
33+
app: nginx
34+
spec:
35+
containers:
36+
- name: patch-demo-ctr
37+
image: nginx
38+
tolerations:
39+
- effect: NoSchedule
40+
key: dedicated
41+
value: test-team

0 commit comments

Comments
 (0)