Skip to content

Commit 8d53ef3

Browse files
Add pr template command (#485)
* Add pr template command Will be used for on-demand pr gen * switch to liquid templating to remain consistent w/ api
1 parent 3861ee3 commit 8d53ef3

8 files changed

Lines changed: 294 additions & 49 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@ jobs:
1919
steps:
2020
- name: Checkout
2121
uses: actions/checkout@v3
22-
- name: Configure AWS Credentials
23-
uses: aws-actions/configure-aws-credentials@v4
24-
with:
25-
aws-region: us-east-2
26-
role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments
27-
role-session-name: PluralCLI
28-
- name: setup kubectl
29-
uses: azure/setup-kubectl@v3
30-
- name: Get EKS credentials
31-
run: aws eks update-kubeconfig --name pluraldev
22+
# - name: Configure AWS Credentials
23+
# uses: aws-actions/configure-aws-credentials@v4
24+
# with:
25+
# aws-region: us-east-2
26+
# role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments
27+
# role-session-name: PluralCLI
28+
# - name: setup kubectl
29+
# uses: azure/setup-kubectl@v3
30+
# - name: Get EKS credentials
31+
# run: aws eks update-kubeconfig --name pluraldev
3232
- name: Docker meta
3333
id: meta
3434
uses: docker/metadata-action@v4
@@ -41,34 +41,38 @@ jobs:
4141
type=sha
4242
type=ref,event=pr
4343
type=ref,event=branch
44+
# - name: Set up Docker Buildx
45+
# id: builder
46+
# uses: docker/setup-buildx-action@v3
47+
# with:
48+
# # cleanup: true
49+
# # driver: kubernetes
50+
# platforms: linux/amd64
51+
# driver-opts: |
52+
# namespace=buildx
53+
# requests.cpu=1.5
54+
# requests.memory=3.5Gi
55+
# "nodeselector=plural.sh/scalingGroup=buildx-spot-x86"
56+
# "tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule"
57+
# - name: Append ARM buildx builder from AWS
58+
# run: |
59+
# docker buildx create \
60+
# --append \
61+
# --bootstrap \
62+
# --name ${{ steps.builder.outputs.name }} \
63+
# --driver=kubernetes \
64+
# --platform linux/arm64 \
65+
# --node=${{ steps.builder.outputs.name }}-arm64 \
66+
# --buildkitd-flags "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host" \
67+
# --driver-opt namespace=buildx \
68+
# --driver-opt requests.cpu=1.5 \
69+
# --driver-opt requests.memory=3.5Gi \
70+
# '--driver-opt="nodeselector=plural.sh/scalingGroup=buildx-spot-arm64"' \
71+
# '--driver-opt="tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule"'
72+
- name: Set up QEMU
73+
uses: docker/setup-qemu-action@v3
4474
- name: Set up Docker Buildx
45-
id: builder
4675
uses: docker/setup-buildx-action@v3
47-
with:
48-
cleanup: true
49-
driver: kubernetes
50-
platforms: linux/amd64
51-
driver-opts: |
52-
namespace=buildx
53-
requests.cpu=1.5
54-
requests.memory=3.5Gi
55-
"nodeselector=plural.sh/scalingGroup=buildx-spot-x86"
56-
"tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule"
57-
- name: Append ARM buildx builder from AWS
58-
run: |
59-
docker buildx create \
60-
--append \
61-
--bootstrap \
62-
--name ${{ steps.builder.outputs.name }} \
63-
--driver=kubernetes \
64-
--platform linux/arm64 \
65-
--node=${{ steps.builder.outputs.name }}-arm64 \
66-
--buildkitd-flags "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host" \
67-
--driver-opt namespace=buildx \
68-
--driver-opt requests.cpu=1.5 \
69-
--driver-opt requests.memory=3.5Gi \
70-
'--driver-opt="nodeselector=plural.sh/scalingGroup=buildx-spot-arm64"' \
71-
'--driver-opt="tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule"'
7276
- name: Login to GHCR
7377
uses: docker/login-action@v2
7478
with:
@@ -107,19 +111,19 @@ jobs:
107111
uses: github/codeql-action/upload-sarif@v2
108112
with:
109113
sarif_file: 'trivy-results.sarif'
110-
- name: Configure AWS Credentials
111-
uses: aws-actions/configure-aws-credentials@v4
112-
if: always()
113-
with:
114-
aws-region: us-east-2
115-
role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments
116-
role-session-name: PluralCLI
117-
- name: Manually cleanup buildx
118-
if: always()
119-
run: |
120-
docker buildx stop ${{ steps.builder.outputs.name }}
121-
sleep 10
122-
docker buildx rm ${{ steps.builder.outputs.name }}
114+
# - name: Configure AWS Credentials
115+
# uses: aws-actions/configure-aws-credentials@v4
116+
# if: always()
117+
# with:
118+
# aws-region: us-east-2
119+
# role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments
120+
# role-session-name: PluralCLI
121+
# - name: Manually cleanup buildx
122+
# if: always()
123+
# run: |
124+
# docker buildx stop ${{ steps.builder.outputs.name }}
125+
# sleep 10
126+
# docker buildx rm ${{ steps.builder.outputs.name }}
123127
cloud:
124128
name: Build cloud image
125129
runs-on: ubuntu-latest

cmd/plural/plural.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,13 @@ func (p *Plural) getCommands() []cli.Command {
504504
},
505505
Category: "CD",
506506
},
507+
{
508+
Name: "pull-requests",
509+
Aliases: []string{"pr"},
510+
Usage: "Generate and manage pull requests",
511+
Subcommands: prCommands(),
512+
Category: "CD",
513+
},
507514
{
508515
Name: "template",
509516
Aliases: []string{"tpl"},

cmd/plural/pr.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package plural
2+
3+
import (
4+
"github.com/pluralsh/plural-cli/pkg/pr"
5+
"github.com/urfave/cli"
6+
)
7+
8+
func prCommands() []cli.Command {
9+
return []cli.Command{
10+
{
11+
Name: "template",
12+
Usage: "applies a pr template resource in the local source tree",
13+
Action: handlePrTemplate,
14+
Flags: []cli.Flag{
15+
cli.StringFlag{
16+
Name: "file",
17+
Usage: "the file the template was placed in",
18+
Required: true,
19+
},
20+
},
21+
},
22+
}
23+
}
24+
25+
func handlePrTemplate(c *cli.Context) error {
26+
template, err := pr.Build(c.String("file"))
27+
if err != nil {
28+
return err
29+
}
30+
31+
return pr.Apply(template)
32+
}

pkg/pr/apply.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package pr
2+
3+
func Apply(template *PrTemplate) error {
4+
if err := applyUpdates(template.Spec.Updates, template.Context); err != nil {
5+
return err
6+
}
7+
8+
return applyCreates(template.Spec.Creates, template.Context)
9+
}

pkg/pr/creates.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package pr
2+
3+
func applyCreates(creates *CreateSpec, ctx map[string]interface{}) error {
4+
return nil
5+
}

pkg/pr/types.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package pr
2+
3+
import (
4+
"os"
5+
6+
"sigs.k8s.io/yaml"
7+
)
8+
9+
type PrTemplate struct {
10+
ApiVersion string `json:"apiVersion"`
11+
Kind string `json:"kind"`
12+
Metadata map[string]interface{} `json:"metadata"`
13+
Context map[string]interface{} `json:"context"`
14+
Spec PrTemplateSpec `json:"spec"`
15+
}
16+
17+
type PrTemplateSpec struct {
18+
Updates *UpdateSpec `json:"updates"`
19+
Creates *CreateSpec `json:"creates"`
20+
}
21+
22+
type UpdateSpec struct {
23+
Regexes []string `json:"regexes"`
24+
Files []string `json:"files"`
25+
ReplaceTemplate string `json:"replace_template"`
26+
Yq string `json:"yq"`
27+
MatchStrategy string `json:"match_strategy"`
28+
}
29+
30+
type CreateSpec struct {
31+
}
32+
33+
func Build(path string) (*PrTemplate, error) {
34+
pr := &PrTemplate{}
35+
data, err := os.ReadFile(path)
36+
if err != nil {
37+
return pr, err
38+
}
39+
40+
if err := yaml.Unmarshal(data, pr); err != nil {
41+
return pr, err
42+
}
43+
44+
return pr, nil
45+
}

pkg/pr/updates.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package pr
2+
3+
import (
4+
"io/fs"
5+
"path/filepath"
6+
"regexp"
7+
)
8+
9+
func applyUpdates(updates *UpdateSpec, ctx map[string]interface{}) error {
10+
replacement, err := templateReplacement([]byte(updates.ReplaceTemplate), ctx)
11+
if err != nil {
12+
return err
13+
}
14+
15+
return filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
16+
if err != nil {
17+
return err
18+
}
19+
20+
if info.IsDir() {
21+
return nil
22+
}
23+
24+
ok, err := filenameMatches(path, updates.Files)
25+
if err != nil {
26+
return err
27+
}
28+
29+
if ok {
30+
return updateFile(path, updates, replacement)
31+
}
32+
33+
return nil
34+
})
35+
}
36+
37+
func updateFile(path string, updates *UpdateSpec, replacement []byte) error {
38+
switch updates.MatchStrategy {
39+
case "any":
40+
return anyUpdateFile(path, updates, replacement)
41+
case "all":
42+
return allUpdateFile(path, updates)
43+
case "recursive":
44+
return recursiveUpdateFile(path, updates, replacement)
45+
default:
46+
return nil
47+
}
48+
}
49+
50+
func anyUpdateFile(path string, updates *UpdateSpec, replacement []byte) error {
51+
return replaceInPlace(path, func(data []byte) ([]byte, error) {
52+
for _, reg := range updates.Regexes {
53+
r, err := regexp.Compile(reg)
54+
if err != nil {
55+
return data, err
56+
}
57+
data = r.ReplaceAll(data, replacement)
58+
}
59+
return data, nil
60+
})
61+
}
62+
63+
func allUpdateFile(path string, updates *UpdateSpec) error {
64+
return nil
65+
}
66+
67+
func recursiveUpdateFile(path string, updates *UpdateSpec, replacement []byte) error {
68+
return replaceInPlace(path, func(data []byte) ([]byte, error) {
69+
return recursiveReplace(data, updates.Regexes, replacement)
70+
})
71+
}
72+
73+
func recursiveReplace(data []byte, regexes []string, replacement []byte) ([]byte, error) {
74+
if len(regexes) == 0 {
75+
return []byte(replacement), nil
76+
}
77+
78+
r, err := regexp.Compile(regexes[0])
79+
if err != nil {
80+
return data, err
81+
}
82+
83+
res := r.ReplaceAllFunc(data, func(d []byte) []byte {
84+
res, err := recursiveReplace(d, regexes[1:], replacement)
85+
if err != nil {
86+
panic(err)
87+
}
88+
return res
89+
})
90+
91+
return res, nil
92+
}
93+
94+
func filenameMatches(path string, files []string) (bool, error) {
95+
for _, f := range files {
96+
r, err := regexp.Compile(f)
97+
if err != nil {
98+
return false, err
99+
}
100+
101+
if r.MatchString(path) {
102+
return true, nil
103+
}
104+
}
105+
106+
return false, nil
107+
}

pkg/pr/utils.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package pr
2+
3+
import (
4+
"os"
5+
6+
"github.com/osteele/liquid"
7+
)
8+
9+
var (
10+
liquidEngine = liquid.NewEngine()
11+
)
12+
13+
func templateReplacement(data []byte, ctx map[string]interface{}) ([]byte, error) {
14+
bindings := map[string]interface{}{
15+
"context": ctx,
16+
}
17+
return liquidEngine.ParseAndRender(data, bindings)
18+
}
19+
20+
func replaceInPlace(path string, rep func(data []byte) ([]byte, error)) error {
21+
info, err := os.Stat(path)
22+
if err != nil {
23+
return err
24+
}
25+
26+
data, err := os.ReadFile(path)
27+
if err != nil {
28+
return err
29+
}
30+
31+
resData, err := rep(data)
32+
if err != nil {
33+
return err
34+
}
35+
return os.WriteFile(path, resData, info.Mode())
36+
}

0 commit comments

Comments
 (0)