Skip to content

Commit 08e993c

Browse files
committed
feat: add patches to kubectl deployments
Signed-off-by: Luca Di Maio <luca.dimaio1@gmail.com>
1 parent dadfe16 commit 08e993c

11 files changed

Lines changed: 465 additions & 80 deletions

File tree

e2e/tests/deploy/deploy.go

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ package deploy
22

33
import (
44
"context"
5-
"github.com/onsi/ginkgo/v2"
65
"os"
76
"path/filepath"
87
"strings"
98

9+
"github.com/onsi/ginkgo/v2"
10+
1011
"github.com/loft-sh/devspace/cmd"
1112
"github.com/loft-sh/devspace/cmd/flags"
1213
"github.com/loft-sh/devspace/e2e/framework"
1314
"github.com/loft-sh/devspace/e2e/kube"
1415
"github.com/loft-sh/devspace/pkg/devspace/kubectl"
1516
"github.com/loft-sh/devspace/pkg/util/factory"
17+
v1 "k8s.io/api/core/v1"
1618
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1719
)
1820

@@ -438,6 +440,109 @@ var _ = DevSpaceDescribe("deploy", func() {
438440
framework.ExpectEqual(out, "test")
439441
})
440442

443+
//nolint:dupl
444+
ginkgo.It("should deploy kubectl application with patches", func() {
445+
tempDir, err := framework.CopyToTempDir("tests/deploy/testdata/kubectl_patches")
446+
framework.ExpectNoError(err)
447+
defer framework.CleanupTempDir(initialDir, tempDir)
448+
449+
ns, err := kubeClient.CreateNamespace("deploy")
450+
framework.ExpectNoError(err)
451+
defer func() {
452+
err := kubeClient.DeleteNamespace(ns)
453+
framework.ExpectNoError(err)
454+
}()
455+
456+
// create a new dev command
457+
deployCmd := &cmd.RunPipelineCmd{
458+
GlobalFlags: &flags.GlobalFlags{
459+
NoWarn: true,
460+
Namespace: ns,
461+
},
462+
Pipeline: "deploy",
463+
}
464+
465+
// run the command
466+
err = deployCmd.RunDefault(f)
467+
framework.ExpectNoError(err)
468+
469+
// check if services are there
470+
service, err := kubeClient.RawClient().CoreV1().Services(ns).Get(context.TODO(), "nginx-deployment", metav1.GetOptions{})
471+
framework.ExpectNoError(err)
472+
// check if patches are correctly applied to service
473+
framework.ExpectEqual(service.Labels["test"], "test234")
474+
framework.ExpectEqual(service.Spec.Ports[0].Port, int32(8080))
475+
476+
// check that container is correctly deployed
477+
out, err := kubeClient.ExecByImageSelector("nginx", ns, []string{"echo", "-n", "test"})
478+
framework.ExpectNoError(err)
479+
framework.ExpectEqual(out, "test")
480+
481+
deployment, err := kubeClient.RawClient().AppsV1().Deployments(ns).Get(context.TODO(), "nginx-deployment", metav1.GetOptions{})
482+
framework.ExpectNoError(err)
483+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[0].Name, "nginx")
484+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[0].Image, "nginx:1.23.3")
485+
framework.ExpectEqual(deployment.Spec.Template.GetObjectMeta().GetLabels(), map[string]string {"app": "nginx", "test": "test123"})
486+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[1].Name, "busybox")
487+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[2].Name, "alpine")
488+
// Ensure the wildcard works
489+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[0].Env[0], v1.EnvVar{Name: "test", Value: "test123"})
490+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[1].Env[0], v1.EnvVar{Name: "test", Value: "test123"})
491+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[2].Env[0], v1.EnvVar{Name: "test", Value: "test123"})
492+
})
493+
494+
//nolint:dupl
495+
ginkgo.It("should deploy kubectl inline manifest application with patches", func() {
496+
tempDir, err := framework.CopyToTempDir("tests/deploy/testdata/kubectl_inline_manifest_patches")
497+
framework.ExpectNoError(err)
498+
defer framework.CleanupTempDir(initialDir, tempDir)
499+
500+
ns, err := kubeClient.CreateNamespace("deploy")
501+
framework.ExpectNoError(err)
502+
defer func() {
503+
err := kubeClient.DeleteNamespace(ns)
504+
framework.ExpectNoError(err)
505+
}()
506+
507+
// create a new dev command
508+
deployCmd := &cmd.RunPipelineCmd{
509+
GlobalFlags: &flags.GlobalFlags{
510+
NoWarn: true,
511+
Namespace: ns,
512+
},
513+
Pipeline: "deploy",
514+
}
515+
516+
// run the command
517+
err = deployCmd.RunDefault(f)
518+
framework.ExpectNoError(err)
519+
520+
// check if services are there
521+
service, err := kubeClient.RawClient().CoreV1().Services(ns).Get(context.TODO(), "nginx-inline-deployment", metav1.GetOptions{})
522+
framework.ExpectNoError(err)
523+
// check if patches are correctly applied to service
524+
framework.ExpectEqual(service.Labels["test"], "test234")
525+
framework.ExpectEqual(service.Spec.Ports[0].Port, int32(8080))
526+
527+
// check that container is correctly deployed
528+
out, err := kubeClient.ExecByImageSelector("nginx", ns, []string{"echo", "-n", "test"})
529+
framework.ExpectNoError(err)
530+
framework.ExpectEqual(out, "test")
531+
532+
deployment, err := kubeClient.RawClient().AppsV1().Deployments(ns).Get(context.TODO(), "nginx-inline-deployment", metav1.GetOptions{})
533+
framework.ExpectNoError(err)
534+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[0].Name, "nginx")
535+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[0].Image, "nginx:1.23.3")
536+
framework.ExpectEqual(deployment.Spec.Template.GetObjectMeta().GetLabels(), map[string]string {"app": "nginx", "test": "test123"})
537+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[1].Name, "busybox")
538+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[2].Name, "alpine")
539+
// Ensure the wildcard works
540+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[0].Env[0], v1.EnvVar{Name: "test", Value: "test123"})
541+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[1].Env[0], v1.EnvVar{Name: "test", Value: "test123"})
542+
framework.ExpectEqual(deployment.Spec.Template.Spec.Containers[2].Env[0], v1.EnvVar{Name: "test", Value: "test123"})
543+
})
544+
545+
441546
ginkgo.It("should deploy helm chart from specific branch in git repo", func() {
442547
tempDir, err := framework.CopyToTempDir("tests/deploy/testdata/helm_git_branch")
443548
framework.ExpectNoError(err)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
version: v2beta1
2+
name: nginx-k8s
3+
4+
deployments:
5+
example:
6+
kubectl:
7+
inlineManifest: |-
8+
apiVersion: apps/v1
9+
kind: Deployment
10+
metadata:
11+
name: nginx-inline-deployment
12+
spec:
13+
selector:
14+
matchLabels:
15+
app: nginx
16+
replicas: 4
17+
template:
18+
metadata:
19+
labels:
20+
app: nginx
21+
spec:
22+
containers:
23+
- name: nginx
24+
image: nginx:1.14.2
25+
ports:
26+
- containerPort: 80
27+
- name: busybox
28+
image: busybox
29+
command: ["sleep"]
30+
args: ["infinity"]
31+
- name: alpine
32+
image: alpine
33+
command: ["sleep"]
34+
args: ["infinity"]
35+
---
36+
apiVersion: v1
37+
kind: Service
38+
metadata:
39+
name: nginx-inline-deployment
40+
labels:
41+
app: nginx-inline-deployment
42+
spec:
43+
ports:
44+
- port: 80
45+
protocol: TCP
46+
selector:
47+
app: nginx-inline-deployment
48+
49+
patches:
50+
- target:
51+
apiVersion: apps/v1 # Optional
52+
kind: Deployment # Optional
53+
name: nginx-inline-deployment # Required
54+
op: replace
55+
path: spec.template.spec.containers[0].image
56+
value: nginx:1.23.3
57+
- target:
58+
apiVersion: apps/v1 # Optional
59+
kind: Deployment # Optional
60+
name: nginx-inline-deployment # Required
61+
op: remove
62+
path: spec.replicas
63+
- target:
64+
apiVersion: apps/v1 # Optional
65+
kind: Deployment # Optional
66+
name: nginx-inline-deployment # Required
67+
op: add
68+
path: spec.template.metadata.labels.test
69+
value: test123
70+
- target:
71+
apiVersion: v1 # Optional
72+
kind: Service # Optional
73+
name: nginx-inline-deployment # Required
74+
op: replace
75+
path: spec.ports[0].port
76+
value: 8080
77+
- target:
78+
apiVersion: v1 # Optional
79+
kind: Service # Optional
80+
name: nginx-inline-deployment # Required
81+
op: add
82+
path: metadata.labels.test
83+
value: test234
84+
# wildcard match
85+
- target:
86+
apiVersion: apps/v1 # Optional
87+
kind: Deployment # Optional
88+
name: nginx-inline-deployment # Required
89+
op: add
90+
path: spec.template.spec.containers[*].env
91+
value: [{"name": "test", "value": "test123"}]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: nginx-deployment
5+
spec:
6+
selector:
7+
matchLabels:
8+
app: nginx
9+
replicas: 4
10+
template:
11+
metadata:
12+
labels:
13+
app: nginx
14+
spec:
15+
containers:
16+
- name: nginx
17+
image: nginx:1.14.2
18+
ports:
19+
- containerPort: 80
20+
- name: busybox
21+
image: busybox
22+
command: ["sleep"]
23+
args: ["infinity"]
24+
- name: alpine
25+
image: alpine
26+
command: ["sleep"]
27+
args: ["infinity"]
28+
---
29+
apiVersion: v1
30+
kind: Service
31+
metadata:
32+
name: nginx-deployment
33+
labels:
34+
app: nginx-deployment
35+
spec:
36+
ports:
37+
- port: 80
38+
protocol: TCP
39+
selector:
40+
app: nginx-deployment
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
version: v2beta1
2+
name: nginx-k8s
3+
4+
deployments:
5+
example:
6+
kubectl:
7+
manifests:
8+
- ./deployment.yaml
9+
patches:
10+
# match specific container
11+
- target:
12+
apiVersion: apps/v1 # Optional
13+
kind: Deployment # Optional
14+
name: nginx-deployment # Required
15+
op: replace
16+
path: spec.template.spec.containers[0].image
17+
value: nginx:1.23.3
18+
- target:
19+
apiVersion: apps/v1 # Optional
20+
kind: Deployment # Optional
21+
name: nginx-deployment # Required
22+
op: remove
23+
path: spec.replicas
24+
- target:
25+
apiVersion: apps/v1 # Optional
26+
kind: Deployment # Optional
27+
name: nginx-deployment # Required
28+
op: add
29+
path: spec.template.metadata.labels.test
30+
value: test123
31+
- target:
32+
apiVersion: v1 # Optional
33+
kind: Service # Optional
34+
name: nginx-deployment # Required
35+
op: replace
36+
path: spec.ports[0].port
37+
value: 8080
38+
- target:
39+
apiVersion: v1 # Optional
40+
kind: Service # Optional
41+
name: nginx-deployment # Required
42+
op: add
43+
path: metadata.labels.test
44+
value: test234
45+
# wildcard match
46+
- target:
47+
apiVersion: apps/v1 # Optional
48+
kind: Deployment # Optional
49+
name: nginx-deployment # Required
50+
op: add
51+
path: spec.template.spec.containers[*].env
52+
value: [{"name": "test", "value": "test123"}]

pkg/devspace/config/loader/patch/patch.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package patch
22

33
import (
44
"fmt"
5+
"regexp"
6+
"strings"
7+
58
"github.com/loft-sh/devspace/pkg/util/yamlutil"
69

710
"gopkg.in/yaml.v3"
@@ -41,3 +44,44 @@ func NewNode(raw *interface{}) (*yaml.Node, error) {
4144

4245
return &node, nil
4346
}
47+
48+
func TransformPath(path string) string {
49+
if path == "" {
50+
return path
51+
}
52+
53+
legacyExtendedSyntaxRegEx := regexp.MustCompile(`(?i)([^\=]+)=([^\.\=\>\<\~]+)`)
54+
hasFilterRegEx := regexp.MustCompile(`(?i)\[\?.*\)\]`)
55+
indexXPathRegEx := regexp.MustCompile(`\/(\d+|\*)\/`)
56+
trailingIndexXPathRegEx := regexp.MustCompile(`\/(\d+|\*)$`)
57+
rootXPathRegEx := regexp.MustCompile(`^\/`)
58+
numeric := regexp.MustCompile(`^\d+$`)
59+
rewrittenPath := path
60+
61+
if legacyExtendedSyntaxRegEx.MatchString(path) {
62+
// Using property=value selectors
63+
rewriteTokens := []string{}
64+
tokens := strings.Split(path, ".")
65+
for _, token := range tokens {
66+
rewriteToken := token
67+
if legacyExtendedSyntaxRegEx.MatchString(token) {
68+
filterTokens := legacyExtendedSyntaxRegEx.FindStringSubmatch(token)
69+
if numeric.MatchString((filterTokens[2])) {
70+
rewriteToken = fmt.Sprintf("[?(@.%s=='%s' || @.%s==%s)]", filterTokens[1], filterTokens[2], filterTokens[1], filterTokens[2])
71+
} else {
72+
rewriteToken = fmt.Sprintf("[?(@.%s=='%s')]", filterTokens[1], filterTokens[2])
73+
}
74+
}
75+
rewriteTokens = append(rewriteTokens, rewriteToken)
76+
}
77+
rewrittenPath = strings.Join(rewriteTokens, ".")
78+
rewrittenPath = strings.ReplaceAll(rewrittenPath, ".[?", "[?")
79+
} else if strings.Contains(path, "/") && !hasFilterRegEx.MatchString(path) {
80+
// Is XPath
81+
rewrittenPath = indexXPathRegEx.ReplaceAllString(path, "[$1].")
82+
rewrittenPath = trailingIndexXPathRegEx.ReplaceAllString(rewrittenPath, "[$1]")
83+
rewrittenPath = rootXPathRegEx.ReplaceAllLiteralString(rewrittenPath, "$.")
84+
}
85+
86+
return rewrittenPath
87+
}

0 commit comments

Comments
 (0)