From 8d53ef3aa7db93c9d3739d43b6931bb37e530248 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Tue, 23 Jan 2024 13:30:55 -0500 Subject: [PATCH] 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 --- .github/workflows/ci.yaml | 102 +++++++++++++++++++----------------- cmd/plural/plural.go | 7 +++ cmd/plural/pr.go | 32 ++++++++++++ pkg/pr/apply.go | 9 ++++ pkg/pr/creates.go | 5 ++ pkg/pr/types.go | 45 ++++++++++++++++ pkg/pr/updates.go | 107 ++++++++++++++++++++++++++++++++++++++ pkg/pr/utils.go | 36 +++++++++++++ 8 files changed, 294 insertions(+), 49 deletions(-) create mode 100644 cmd/plural/pr.go create mode 100644 pkg/pr/apply.go create mode 100644 pkg/pr/creates.go create mode 100644 pkg/pr/types.go create mode 100644 pkg/pr/updates.go create mode 100644 pkg/pr/utils.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4b27a8a8..583d7a0f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,16 +19,16 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: us-east-2 - role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments - role-session-name: PluralCLI - - name: setup kubectl - uses: azure/setup-kubectl@v3 - - name: Get EKS credentials - run: aws eks update-kubeconfig --name pluraldev + # - name: Configure AWS Credentials + # uses: aws-actions/configure-aws-credentials@v4 + # with: + # aws-region: us-east-2 + # role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments + # role-session-name: PluralCLI + # - name: setup kubectl + # uses: azure/setup-kubectl@v3 + # - name: Get EKS credentials + # run: aws eks update-kubeconfig --name pluraldev - name: Docker meta id: meta uses: docker/metadata-action@v4 @@ -41,34 +41,38 @@ jobs: type=sha type=ref,event=pr type=ref,event=branch + # - name: Set up Docker Buildx + # id: builder + # uses: docker/setup-buildx-action@v3 + # with: + # # cleanup: true + # # driver: kubernetes + # platforms: linux/amd64 + # driver-opts: | + # namespace=buildx + # requests.cpu=1.5 + # requests.memory=3.5Gi + # "nodeselector=plural.sh/scalingGroup=buildx-spot-x86" + # "tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule" + # - name: Append ARM buildx builder from AWS + # run: | + # docker buildx create \ + # --append \ + # --bootstrap \ + # --name ${{ steps.builder.outputs.name }} \ + # --driver=kubernetes \ + # --platform linux/arm64 \ + # --node=${{ steps.builder.outputs.name }}-arm64 \ + # --buildkitd-flags "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host" \ + # --driver-opt namespace=buildx \ + # --driver-opt requests.cpu=1.5 \ + # --driver-opt requests.memory=3.5Gi \ + # '--driver-opt="nodeselector=plural.sh/scalingGroup=buildx-spot-arm64"' \ + # '--driver-opt="tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule"' + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - id: builder uses: docker/setup-buildx-action@v3 - with: - cleanup: true - driver: kubernetes - platforms: linux/amd64 - driver-opts: | - namespace=buildx - requests.cpu=1.5 - requests.memory=3.5Gi - "nodeselector=plural.sh/scalingGroup=buildx-spot-x86" - "tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule" - - name: Append ARM buildx builder from AWS - run: | - docker buildx create \ - --append \ - --bootstrap \ - --name ${{ steps.builder.outputs.name }} \ - --driver=kubernetes \ - --platform linux/arm64 \ - --node=${{ steps.builder.outputs.name }}-arm64 \ - --buildkitd-flags "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host" \ - --driver-opt namespace=buildx \ - --driver-opt requests.cpu=1.5 \ - --driver-opt requests.memory=3.5Gi \ - '--driver-opt="nodeselector=plural.sh/scalingGroup=buildx-spot-arm64"' \ - '--driver-opt="tolerations=key=plural.sh/capacityType,value=SPOT,effect=NoSchedule;key=plural.sh/reserved,value=BUILDX,effect=NoSchedule"' - name: Login to GHCR uses: docker/login-action@v2 with: @@ -107,19 +111,19 @@ jobs: uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - if: always() - with: - aws-region: us-east-2 - role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments - role-session-name: PluralCLI - - name: Manually cleanup buildx - if: always() - run: | - docker buildx stop ${{ steps.builder.outputs.name }} - sleep 10 - docker buildx rm ${{ steps.builder.outputs.name }} + # - name: Configure AWS Credentials + # uses: aws-actions/configure-aws-credentials@v4 + # if: always() + # with: + # aws-region: us-east-2 + # role-to-assume: arn:aws:iam::312272277431:role/github-actions/buildx-deployments + # role-session-name: PluralCLI + # - name: Manually cleanup buildx + # if: always() + # run: | + # docker buildx stop ${{ steps.builder.outputs.name }} + # sleep 10 + # docker buildx rm ${{ steps.builder.outputs.name }} cloud: name: Build cloud image runs-on: ubuntu-latest diff --git a/cmd/plural/plural.go b/cmd/plural/plural.go index be87eb7a..0cd61643 100644 --- a/cmd/plural/plural.go +++ b/cmd/plural/plural.go @@ -504,6 +504,13 @@ func (p *Plural) getCommands() []cli.Command { }, Category: "CD", }, + { + Name: "pull-requests", + Aliases: []string{"pr"}, + Usage: "Generate and manage pull requests", + Subcommands: prCommands(), + Category: "CD", + }, { Name: "template", Aliases: []string{"tpl"}, diff --git a/cmd/plural/pr.go b/cmd/plural/pr.go new file mode 100644 index 00000000..0df76870 --- /dev/null +++ b/cmd/plural/pr.go @@ -0,0 +1,32 @@ +package plural + +import ( + "github.com/pluralsh/plural-cli/pkg/pr" + "github.com/urfave/cli" +) + +func prCommands() []cli.Command { + return []cli.Command{ + { + Name: "template", + Usage: "applies a pr template resource in the local source tree", + Action: handlePrTemplate, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "the file the template was placed in", + Required: true, + }, + }, + }, + } +} + +func handlePrTemplate(c *cli.Context) error { + template, err := pr.Build(c.String("file")) + if err != nil { + return err + } + + return pr.Apply(template) +} diff --git a/pkg/pr/apply.go b/pkg/pr/apply.go new file mode 100644 index 00000000..aa5ff8ff --- /dev/null +++ b/pkg/pr/apply.go @@ -0,0 +1,9 @@ +package pr + +func Apply(template *PrTemplate) error { + if err := applyUpdates(template.Spec.Updates, template.Context); err != nil { + return err + } + + return applyCreates(template.Spec.Creates, template.Context) +} diff --git a/pkg/pr/creates.go b/pkg/pr/creates.go new file mode 100644 index 00000000..6b14e454 --- /dev/null +++ b/pkg/pr/creates.go @@ -0,0 +1,5 @@ +package pr + +func applyCreates(creates *CreateSpec, ctx map[string]interface{}) error { + return nil +} diff --git a/pkg/pr/types.go b/pkg/pr/types.go new file mode 100644 index 00000000..dcecdd8a --- /dev/null +++ b/pkg/pr/types.go @@ -0,0 +1,45 @@ +package pr + +import ( + "os" + + "sigs.k8s.io/yaml" +) + +type PrTemplate struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata map[string]interface{} `json:"metadata"` + Context map[string]interface{} `json:"context"` + Spec PrTemplateSpec `json:"spec"` +} + +type PrTemplateSpec struct { + Updates *UpdateSpec `json:"updates"` + Creates *CreateSpec `json:"creates"` +} + +type UpdateSpec struct { + Regexes []string `json:"regexes"` + Files []string `json:"files"` + ReplaceTemplate string `json:"replace_template"` + Yq string `json:"yq"` + MatchStrategy string `json:"match_strategy"` +} + +type CreateSpec struct { +} + +func Build(path string) (*PrTemplate, error) { + pr := &PrTemplate{} + data, err := os.ReadFile(path) + if err != nil { + return pr, err + } + + if err := yaml.Unmarshal(data, pr); err != nil { + return pr, err + } + + return pr, nil +} diff --git a/pkg/pr/updates.go b/pkg/pr/updates.go new file mode 100644 index 00000000..c1963af2 --- /dev/null +++ b/pkg/pr/updates.go @@ -0,0 +1,107 @@ +package pr + +import ( + "io/fs" + "path/filepath" + "regexp" +) + +func applyUpdates(updates *UpdateSpec, ctx map[string]interface{}) error { + replacement, err := templateReplacement([]byte(updates.ReplaceTemplate), ctx) + if err != nil { + return err + } + + return filepath.Walk(".", func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + ok, err := filenameMatches(path, updates.Files) + if err != nil { + return err + } + + if ok { + return updateFile(path, updates, replacement) + } + + return nil + }) +} + +func updateFile(path string, updates *UpdateSpec, replacement []byte) error { + switch updates.MatchStrategy { + case "any": + return anyUpdateFile(path, updates, replacement) + case "all": + return allUpdateFile(path, updates) + case "recursive": + return recursiveUpdateFile(path, updates, replacement) + default: + return nil + } +} + +func anyUpdateFile(path string, updates *UpdateSpec, replacement []byte) error { + return replaceInPlace(path, func(data []byte) ([]byte, error) { + for _, reg := range updates.Regexes { + r, err := regexp.Compile(reg) + if err != nil { + return data, err + } + data = r.ReplaceAll(data, replacement) + } + return data, nil + }) +} + +func allUpdateFile(path string, updates *UpdateSpec) error { + return nil +} + +func recursiveUpdateFile(path string, updates *UpdateSpec, replacement []byte) error { + return replaceInPlace(path, func(data []byte) ([]byte, error) { + return recursiveReplace(data, updates.Regexes, replacement) + }) +} + +func recursiveReplace(data []byte, regexes []string, replacement []byte) ([]byte, error) { + if len(regexes) == 0 { + return []byte(replacement), nil + } + + r, err := regexp.Compile(regexes[0]) + if err != nil { + return data, err + } + + res := r.ReplaceAllFunc(data, func(d []byte) []byte { + res, err := recursiveReplace(d, regexes[1:], replacement) + if err != nil { + panic(err) + } + return res + }) + + return res, nil +} + +func filenameMatches(path string, files []string) (bool, error) { + for _, f := range files { + r, err := regexp.Compile(f) + if err != nil { + return false, err + } + + if r.MatchString(path) { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/pr/utils.go b/pkg/pr/utils.go new file mode 100644 index 00000000..75d3c0e0 --- /dev/null +++ b/pkg/pr/utils.go @@ -0,0 +1,36 @@ +package pr + +import ( + "os" + + "github.com/osteele/liquid" +) + +var ( + liquidEngine = liquid.NewEngine() +) + +func templateReplacement(data []byte, ctx map[string]interface{}) ([]byte, error) { + bindings := map[string]interface{}{ + "context": ctx, + } + return liquidEngine.ParseAndRender(data, bindings) +} + +func replaceInPlace(path string, rep func(data []byte) ([]byte, error)) error { + info, err := os.Stat(path) + if err != nil { + return err + } + + data, err := os.ReadFile(path) + if err != nil { + return err + } + + resData, err := rep(data) + if err != nil { + return err + } + return os.WriteFile(path, resData, info.Mode()) +}