This repository was archived by the owner on Jul 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 38
Add utilities for Injecting Kubernetes Deployments #207
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4ff1655
Update gomod
versilis a0242fb
Add injector
versilis d2ae573
Update resources yaml for testing
versilis 642c84d
Add helper for parsing to YAML
versilis 33f8e7a
Update doc comment to be more specific
versilis 5d4daac
Add better docs for InjectableNamespaces method
versilis 48aa8aa
Add better context to returned errors
versilis a3b0183
Factor out deployment conversion into function
versilis 0e144d5
remove redundant conversion
versilis dff7748
Remove redundant uses of deep copy
versilis 8edde25
Replace set implementation with existing library
versilis 12f88e2
Simplify injectable check
versilis 1b26251
Replace mapper implementation with existing library
versilis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package injector | ||
|
|
||
| import ( | ||
| "bytes" | ||
| v1 "k8s.io/api/core/v1" | ||
| kyaml "sigs.k8s.io/yaml" | ||
| ) | ||
|
|
||
| // Calls the given injector's Inject method and returns the result as a YAML bytes. | ||
| func ToRawYAML(injector Injector, sidecar v1.Container) ([]byte, error) { | ||
| injectedObjects, err := injector.Inject(sidecar) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| out := new(bytes.Buffer) | ||
| for _, obj := range injectedObjects { | ||
| raw, err := kyaml.Marshal(obj) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| out.WriteString("---\n") | ||
| out.Write(raw) | ||
| } | ||
|
|
||
| return out.Bytes(), nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| package injector | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "bytes" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/akitasoftware/go-utils/sets" | ||
| "github.com/akitasoftware/go-utils/slices" | ||
| "github.com/pkg/errors" | ||
| appsv1 "k8s.io/api/apps/v1" | ||
| v1 "k8s.io/api/core/v1" | ||
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "k8s.io/apimachinery/pkg/runtime/schema" | ||
| kyamlutil "k8s.io/apimachinery/pkg/util/yaml" | ||
| ) | ||
|
|
||
| type ( | ||
| Injector interface { | ||
| // Injects the given sidecar into all valid Deployment Objects and returns the result as a list of unstructured objects. | ||
| Inject(sidecar v1.Container) ([]*unstructured.Unstructured, error) | ||
| // Returns a list of namespaces that contain injectable objects. | ||
| // This can be used to generate other Kuberenetes objects that need to be created in the same namespace. | ||
| InjectableNamespaces() ([]string, error) | ||
| } | ||
| injectorImpl struct { | ||
| // The list of Kubernetes objects to traverse during injection. This is a list of | ||
| // unstructured objects because we likely won't know the type of all objects | ||
| // ahead of time (e.g., when reading multiple objects from a YAML file). | ||
| objects []*unstructured.Unstructured | ||
| } | ||
| ) | ||
|
|
||
| // Constructs a new Injector with Kubernetes objects derived from the given file path. | ||
| func FromYAML(filePath string) (Injector, error) { | ||
| var err error | ||
| yamlContent, err := getFile(filePath) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to retrieve raw yaml file content") | ||
| } | ||
|
|
||
| // Read the YAML file into a list of unstructured objects. | ||
| // This is necessary because the YAML file may contain multiple Kubernetes objects. | ||
| // We only want to inject the sidecar into Deployment objects, but we still need to parse all resources. | ||
| multidocReader := kyamlutil.NewYAMLReader(bufio.NewReader(bytes.NewReader(yamlContent))) | ||
|
|
||
| var objList []*unstructured.Unstructured | ||
| for { | ||
| raw, err := multidocReader.Read() | ||
| if err != nil { | ||
| if errors.Is(err, io.EOF) { | ||
| break | ||
| } | ||
| return nil, errors.Wrap(err, "failed to read raw yaml file") | ||
| } | ||
|
|
||
| obj, err := fromRawObject(raw) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to convert raw yaml resource to an unstructured object") | ||
| } | ||
|
|
||
| objList = append(objList, obj) | ||
| } | ||
|
|
||
| return &injectorImpl{objects: objList}, nil | ||
| } | ||
|
|
||
| func (i *injectorImpl) InjectableNamespaces() ([]string, error) { | ||
| set := sets.NewSet[string]() | ||
|
|
||
| for _, obj := range i.objects { | ||
| gvk := obj.GetObjectKind().GroupVersionKind() | ||
|
|
||
| if !isInjectable(gvk) { | ||
| continue | ||
| } | ||
|
|
||
| deployment, err := toDeployment(obj) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to convert object to deployment during namespace discovery") | ||
| } | ||
|
|
||
| if deployment.Namespace == "" { | ||
| set.Insert("default") | ||
| } else { | ||
| set.Insert(deployment.Namespace) | ||
| } | ||
| } | ||
|
|
||
| return set.AsSlice(), nil | ||
| } | ||
|
|
||
| func (i *injectorImpl) Inject(sidecar v1.Container) ([]*unstructured.Unstructured, error) { | ||
| onMap := func(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { | ||
| gvk := obj.GetObjectKind().GroupVersionKind() | ||
|
|
||
| if !isInjectable(gvk) { | ||
| return obj, nil | ||
| } | ||
|
|
||
| deployment, err := toDeployment(obj) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to convert object to deployment during injection") | ||
| } | ||
|
|
||
| containers := deployment.Spec.Template.Spec.Containers | ||
| deployment.Spec.Template.Spec.Containers = append(containers, sidecar) | ||
|
|
||
| obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(deployment) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to convert injected deployment to unstructured object") | ||
| } | ||
|
|
||
| return obj, nil | ||
| } | ||
|
|
||
| return slices.MapWithErr(i.objects, onMap) | ||
| } | ||
|
|
||
| func isInjectable(kind schema.GroupVersionKind) bool { | ||
| acceptedKind := schema.GroupVersionKind{ | ||
| Group: "apps", | ||
| Version: "v1", | ||
| Kind: "Deployment", | ||
| } | ||
|
|
||
| return kind == acceptedKind | ||
| } | ||
|
|
||
| // Converts a generic Kubernetes object into a Deployment Object. | ||
| func toDeployment(obj *unstructured.Unstructured) (*appsv1.Deployment, error) { | ||
| var deployment *appsv1.Deployment | ||
|
|
||
| err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to convert object to deployment during injection") | ||
| } | ||
|
|
||
| return deployment, nil | ||
| } | ||
|
|
||
| // fromRawObject converts raw bytes into an unstructured.Unstrucutred object. | ||
| // unstructured.Unstructured is used to represent a Kubernetes object that is not known ahead of time. | ||
| func fromRawObject(raw []byte) (*unstructured.Unstructured, error) { | ||
| jConfigMap, err := kyamlutil.ToJSON(raw) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| object, err := runtime.Decode(unstructured.UnstructuredJSONScheme, jConfigMap) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| unstruct, ok := object.(*unstructured.Unstructured) | ||
| if !ok { | ||
| return nil, errors.New("unstructured conversion failed") | ||
| } | ||
|
|
||
| return unstruct, nil | ||
| } | ||
|
|
||
| func getFile(filePath string) ([]byte, error) { | ||
| fileDir, fileName := filepath.Split(filePath) | ||
|
|
||
| absOutputDir, err := filepath.Abs(fileDir) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Check for directory existence | ||
| if _, staterr := os.Stat(absOutputDir); os.IsNotExist(staterr) { | ||
| return nil, errors.Wrapf(staterr, "directory %s does not exist", absOutputDir) | ||
| } | ||
|
|
||
| absPath := filepath.Join(absOutputDir, fileName) | ||
|
|
||
| // Check for existence of file | ||
| if _, staterr := os.Stat(absPath); os.IsNotExist(staterr) { | ||
| return nil, errors.Wrapf(staterr, "file %s does not exist", absPath) | ||
| } | ||
|
|
||
| return os.ReadFile(absPath) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package injector | ||
|
|
||
| import ( | ||
| "github.com/stretchr/testify/assert" | ||
| appsv1 "k8s.io/api/apps/v1" | ||
| v1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "testing" | ||
| ) | ||
|
|
||
| func Test_Inject(t *testing.T) { | ||
| toUnstructured := func(obj runtime.Object) *unstructured.Unstructured { | ||
| u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| return &unstructured.Unstructured{Object: u} | ||
| } | ||
|
|
||
| appendContainer := func(deployment *appsv1.Deployment, container v1.Container) *appsv1.Deployment { | ||
| injectedDeployment := deployment.DeepCopy() | ||
versilis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| containers := injectedDeployment.Spec.Template.Spec.Containers | ||
| injectedDeployment.Spec.Template.Spec.Containers = append(containers, container) | ||
|
|
||
| return injectedDeployment | ||
| } | ||
|
|
||
| // GIVEN | ||
| dummyDeployment1 := &appsv1.Deployment{ | ||
| TypeMeta: metav1.TypeMeta{ | ||
| Kind: "Deployment", | ||
| APIVersion: "apps/v1", | ||
| }, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "test-deploy-1", | ||
| }, | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Template: v1.PodTemplateSpec{ | ||
| Spec: v1.PodSpec{ | ||
| Containers: []v1.Container{ | ||
| { | ||
| Name: "nginx", | ||
| Image: "nginx", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| dummyDeployment2 := &appsv1.Deployment{ | ||
| TypeMeta: metav1.TypeMeta{ | ||
| Kind: "Deployment", | ||
| APIVersion: "apps/v1", | ||
| }, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "test-deploy-2", | ||
| }, | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Template: v1.PodTemplateSpec{ | ||
| Spec: v1.PodSpec{ | ||
| Containers: []v1.Container{ | ||
| { | ||
| Name: "echo-server", | ||
| Image: "ghcr.io/wzshiming/echoserver/echoserver:v0.0.1", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| sidecar := v1.Container{Name: "sidecar", Image: "fake-image"} | ||
| expectedDeployment1 := appendContainer(dummyDeployment1, sidecar) | ||
| expectedDeployment2 := appendContainer(dummyDeployment2, sidecar) | ||
|
|
||
| injector := injectorImpl{ | ||
| objects: []*unstructured.Unstructured{ | ||
| toUnstructured(dummyDeployment1), | ||
| toUnstructured(dummyDeployment2), | ||
| }, | ||
| } | ||
|
|
||
| expected := []*unstructured.Unstructured{ | ||
| toUnstructured(expectedDeployment1), | ||
| toUnstructured(expectedDeployment2), | ||
| } | ||
|
|
||
| // WHEN | ||
| actual, err := injector.Inject(sidecar) | ||
|
|
||
| // THEN | ||
| if assert.NoError(t, err) { | ||
| assert.Equal(t, expected, actual) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| apiVersion: apps/v1 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't written any unit tests to go alongside this, but it works well for interacting with the CLI |
||
| kind: Deployment | ||
| metadata: | ||
| name: test-deploy | ||
| namespace: default | ||
| spec: | ||
| replicas: 1 | ||
| selector: | ||
| matchLabels: | ||
| app: test-pod | ||
| template: | ||
| metadata: | ||
| labels: | ||
| app: test-pod | ||
| spec: | ||
| containers: | ||
| - name: test-container | ||
| image: ghcr.io/wzshiming/echoserver/echoserver:v0.0.1 | ||
| --- | ||
| apiVersion: apps/v1 | ||
| kind: Deployment | ||
| metadata: | ||
| name: patch-demo | ||
| namespace: ns1 | ||
| spec: | ||
| replicas: 2 | ||
| selector: | ||
| matchLabels: | ||
| app: nginx | ||
| template: | ||
| metadata: | ||
| labels: | ||
| app: nginx | ||
| spec: | ||
| containers: | ||
| - name: patch-demo-ctr | ||
| image: nginx | ||
| tolerations: | ||
| - effect: NoSchedule | ||
| key: dedicated | ||
| value: test-team | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.